Compare commits
6 Commits
Author | SHA1 | Date |
---|---|---|
marceline-cramer | 9a88ec49e9 | |
marceline-cramer | a989c243d3 | |
marceline-cramer | 21ed7f1f45 | |
marceline-cramer | a8abe396de | |
marceline-cramer | eb424f2f84 | |
marceline-cramer | 2d7e71b14a |
|
@ -1 +1,2 @@
|
||||||
|
/instance-example
|
||||||
/target
|
/target
|
||||||
|
|
|
@ -357,6 +357,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"structopt",
|
"structopt",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -18,3 +18,4 @@ serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
structopt = "0.3"
|
structopt = "0.3"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
toml = "0.5"
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
version = "1.16.5"
|
||||||
|
|
||||||
|
[mods.sodium]
|
|
@ -10,9 +10,10 @@ pub struct SearchResponse {
|
||||||
pub total_hits: isize,
|
pub total_hits: isize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Clone, Deserialize, Debug)]
|
||||||
pub struct ModResult {
|
pub struct ModResult {
|
||||||
pub mod_id: String, // TODO parse to `local-xxxxx` with regex
|
pub mod_id: String, // TODO parse to `local-xxxxx` with regex
|
||||||
|
pub slug: String, // not in the API docs, but shows up in queries anyways
|
||||||
pub project_type: Option<String>, // NOTE this isn't in all search results?
|
pub project_type: Option<String>, // NOTE this isn't in all search results?
|
||||||
pub author: String,
|
pub author: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
@ -87,7 +88,7 @@ pub struct ModInfo {
|
||||||
pub struct License {
|
pub struct License {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub url: String,
|
pub url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
|
|
@ -24,7 +24,7 @@ pub enum Command {
|
||||||
#[structopt(setting = structopt::clap::AppSettings::ColoredHelp)]
|
#[structopt(setting = structopt::clap::AppSettings::ColoredHelp)]
|
||||||
Get(SearchArgs),
|
Get(SearchArgs),
|
||||||
#[structopt(setting = structopt::clap::AppSettings::ColoredHelp)]
|
#[structopt(setting = structopt::clap::AppSettings::ColoredHelp)]
|
||||||
Update,
|
Update { instance_dir: Option<PathBuf> },
|
||||||
#[structopt(setting = structopt::clap::AppSettings::ColoredHelp)]
|
#[structopt(setting = structopt::clap::AppSettings::ColoredHelp)]
|
||||||
Clean,
|
Clean,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ModEntry {}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Hopfile {
|
||||||
|
pub version: String,
|
||||||
|
pub mods: HashMap<String, ModEntry>,
|
||||||
|
}
|
174
src/main.rs
174
src/main.rs
|
@ -2,13 +2,16 @@ use futures_util::StreamExt;
|
||||||
use log::*;
|
use log::*;
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod hopfile;
|
||||||
|
|
||||||
use api::*;
|
use api::*;
|
||||||
use config::*;
|
use config::*;
|
||||||
|
use hopfile::*;
|
||||||
|
|
||||||
async fn search_mods(ctx: &AppContext, search_args: &SearchArgs) -> anyhow::Result<SearchResponse> {
|
async fn search_mods(ctx: &AppContext, search_args: &SearchArgs) -> anyhow::Result<SearchResponse> {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
@ -20,13 +23,13 @@ async fn search_mods(ctx: &AppContext, search_args: &SearchArgs) -> anyhow::Resu
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = reqwest::Url::parse_with_params(url.as_str(), ¶ms)?;
|
let url = reqwest::Url::parse_with_params(url.as_str(), ¶ms)?;
|
||||||
info!("GET {}", url);
|
info!("Searching for mods: {}", url);
|
||||||
let response = client
|
let response = client
|
||||||
.get(url)
|
.get(url)
|
||||||
.send()
|
.send()
|
||||||
.await?
|
|
||||||
.json::<SearchResponse>()
|
|
||||||
.await?;
|
.await?;
|
||||||
|
info!("Search results: {:#?}", response);
|
||||||
|
let response = response.json::<SearchResponse>().await?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,10 +47,10 @@ fn display_search_results(ctx: &AppContext, response: &SearchResponse) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO implement enum for more graceful exiting
|
// TODO implement enum for more graceful exiting
|
||||||
async fn select_from_results<'a>(
|
async fn select_from_results(
|
||||||
_ctx: &AppContext,
|
_ctx: &AppContext,
|
||||||
response: &'a SearchResponse,
|
response: &SearchResponse,
|
||||||
) -> anyhow::Result<Vec<&'a ModResult>> {
|
) -> anyhow::Result<Vec<ModResult>> {
|
||||||
let input: String = dialoguer::Input::new()
|
let input: String = dialoguer::Input::new()
|
||||||
.with_prompt("Mods to install (eg: 1 2 3)")
|
.with_prompt("Mods to install (eg: 1 2 3)")
|
||||||
.interact_text()?;
|
.interact_text()?;
|
||||||
|
@ -72,7 +75,10 @@ async fn select_from_results<'a>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(selected.iter().map(|i| &response.hits[*i]).collect())
|
Ok(selected
|
||||||
|
.iter()
|
||||||
|
.map(|i| response.hits[*i].to_owned())
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_mod_info(ctx: &AppContext, mod_result: &ModResult) -> anyhow::Result<ModInfo> {
|
async fn fetch_mod_info(ctx: &AppContext, mod_result: &ModResult) -> anyhow::Result<ModInfo> {
|
||||||
|
@ -83,7 +89,9 @@ async fn fetch_mod_info(ctx: &AppContext, mod_result: &ModResult) -> anyhow::Res
|
||||||
"https://{}/api/v1/mod/{}",
|
"https://{}/api/v1/mod/{}",
|
||||||
ctx.config.upstream.server_address, mod_id
|
ctx.config.upstream.server_address, mod_id
|
||||||
);
|
);
|
||||||
|
info!("Fetching mod info: {}", url);
|
||||||
let response = client.get(url).send().await?;
|
let response = client.get(url).send().await?;
|
||||||
|
info!("Mod info: {:#?}", response);
|
||||||
let response = response.json::<ModInfo>().await?;
|
let response = response.json::<ModInfo>().await?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
@ -94,12 +102,18 @@ async fn fetch_mod_version(ctx: &AppContext, version_id: &String) -> anyhow::Res
|
||||||
"https://{}/api/v1/version/{}",
|
"https://{}/api/v1/version/{}",
|
||||||
ctx.config.upstream.server_address, version_id
|
ctx.config.upstream.server_address, version_id
|
||||||
);
|
);
|
||||||
|
info!("Fetching mod version: {}", url);
|
||||||
let response = client.get(url).send().await?;
|
let response = client.get(url).send().await?;
|
||||||
|
info!("Mod version: {:#?}", response);
|
||||||
let response = response.json::<ModVersion>().await?;
|
let response = response.json::<ModVersion>().await?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_version_file(ctx: &AppContext, file: &ModVersionFile) -> anyhow::Result<()> {
|
async fn download_version_file(
|
||||||
|
ctx: &AppContext,
|
||||||
|
target_dir: &Path,
|
||||||
|
file: &ModVersionFile,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
// TODO replace all uses of .unwrap() with proper error codes
|
// TODO replace all uses of .unwrap() with proper error codes
|
||||||
let filename = &file.filename;
|
let filename = &file.filename;
|
||||||
|
|
||||||
|
@ -117,8 +131,11 @@ async fn download_version_file(ctx: &AppContext, file: &ModVersionFile) -> anyho
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let url = &file.url;
|
let url = &file.url;
|
||||||
|
let filename = target_dir.join(&file.filename);
|
||||||
|
info!("Downloading {} to {:?}", url, filename);
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
let response = client.get(url).send().await?;
|
let response = client.get(url).send().await?;
|
||||||
let total_size = response.content_length().unwrap();
|
let total_size = response.content_length().unwrap();
|
||||||
|
|
||||||
|
@ -129,8 +146,7 @@ async fn download_version_file(ctx: &AppContext, file: &ModVersionFile) -> anyho
|
||||||
pb.set_style(ProgressStyle::default_bar().template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").progress_chars("#>-"));
|
pb.set_style(ProgressStyle::default_bar().template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").progress_chars("#>-"));
|
||||||
pb.set_message(&format!("Downloading {}", url));
|
pb.set_message(&format!("Downloading {}", url));
|
||||||
|
|
||||||
let filename = &file.filename;
|
let mut file = std::fs::File::create(&filename)?;
|
||||||
let mut file = std::fs::File::create(filename)?;
|
|
||||||
let mut downloaded: u64 = 0;
|
let mut downloaded: u64 = 0;
|
||||||
let mut stream = response.bytes_stream();
|
let mut stream = response.bytes_stream();
|
||||||
|
|
||||||
|
@ -143,40 +159,147 @@ async fn download_version_file(ctx: &AppContext, file: &ModVersionFile) -> anyho
|
||||||
pb.set_position(new);
|
pb.set_position(new);
|
||||||
}
|
}
|
||||||
|
|
||||||
pb.finish_with_message(&format!("Downloaded {} to {}", url, filename));
|
pb.finish_with_message(&format!("Downloaded {} to {:#?}", url, filename));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cmd_get(ctx: &AppContext, search_args: SearchArgs) -> anyhow::Result<()> {
|
async fn search_and_select(
|
||||||
|
ctx: &AppContext,
|
||||||
|
search_args: &SearchArgs,
|
||||||
|
) -> anyhow::Result<Vec<ModResult>> {
|
||||||
let response = search_mods(ctx, &search_args).await?;
|
let response = search_mods(ctx, &search_args).await?;
|
||||||
|
|
||||||
if response.hits.is_empty() {
|
if response.hits.is_empty() {
|
||||||
// TODO formatting
|
// TODO formatting
|
||||||
println!("No results; nothing to do...");
|
println!("No results; nothing to do...");
|
||||||
return Ok(());
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add a config file option to disable this
|
||||||
|
if let Some(first_result) = response.hits.first() {
|
||||||
|
if response.hits.len() == 1 {
|
||||||
|
println!("autoselecting only mod result");
|
||||||
|
return Ok(vec![first_result.to_owned()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add a config file option to disable this
|
||||||
|
for mod_info in response.hits.iter() {
|
||||||
|
if mod_info.slug == search_args.package_name {
|
||||||
|
println!("autoselecting mod result slug with exact match");
|
||||||
|
return Ok(vec![mod_info.to_owned()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
display_search_results(ctx, &response);
|
display_search_results(ctx, &response);
|
||||||
let selected = select_from_results(ctx, &response).await?;
|
let selected = loop {
|
||||||
|
let selected = select_from_results(ctx, &response).await?;
|
||||||
|
if selected.is_empty() {
|
||||||
|
use dialoguer::Confirm;
|
||||||
|
let mod_name = &search_args.package_name;
|
||||||
|
let prompt = format!("Really skip downloading {}?", mod_name);
|
||||||
|
let confirm = Confirm::new().with_prompt(prompt).interact()?;
|
||||||
|
if !confirm {
|
||||||
|
println!("Skipping updating {}...", mod_name);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break selected;
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pick_mod_version(
|
||||||
|
ctx: &AppContext,
|
||||||
|
mod_info: &ModInfo,
|
||||||
|
target_versions: &Option<Vec<String>>,
|
||||||
|
) -> anyhow::Result<Option<ModVersion>> {
|
||||||
|
// TODO allow the user to select multiple versions?
|
||||||
|
match target_versions {
|
||||||
|
Some(target_versions) => {
|
||||||
|
for version_id in mod_info.versions.iter() {
|
||||||
|
let version = fetch_mod_version(ctx, version_id).await?;
|
||||||
|
for supported_version in version.game_versions.iter() {
|
||||||
|
if target_versions.contains(supported_version) {
|
||||||
|
return Ok(Some(version));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if let Some(version_id) = mod_info.versions.first() {
|
||||||
|
Ok(Some(fetch_mod_version(ctx, version_id).await?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn download_mods(
|
||||||
|
ctx: &AppContext,
|
||||||
|
target_dir: Option<&Path>,
|
||||||
|
target_versions: &Option<Vec<String>>,
|
||||||
|
mods: &Vec<ModResult>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let current_dir = std::env::current_dir()?;
|
||||||
|
let target_dir = target_dir.unwrap_or(¤t_dir);
|
||||||
|
for to_get in mods.iter() {
|
||||||
|
let mod_info = fetch_mod_info(ctx, to_get).await?;
|
||||||
|
|
||||||
|
let version = pick_mod_version(ctx, &mod_info, target_versions).await?;
|
||||||
|
if let Some(version) = version {
|
||||||
|
for file in version.files.iter() {
|
||||||
|
download_version_file(ctx, &target_dir, file).await?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO message formatting
|
||||||
|
println!("No versions to download for mod {}", mod_info.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cmd_get(ctx: &AppContext, search_args: SearchArgs) -> anyhow::Result<()> {
|
||||||
|
let selected = search_and_select(ctx, &search_args).await?;
|
||||||
if selected.is_empty() {
|
if selected.is_empty() {
|
||||||
// TODO formatting
|
// TODO formatting
|
||||||
println!("No packages selected; nothing to do...");
|
println!("No packages selected; nothing to do...");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
for to_get in selected.iter() {
|
// TODO arg for target directory?
|
||||||
let mod_info = fetch_mod_info(ctx, to_get).await?;
|
download_mods(ctx, None, &search_args.version, &selected).await?;
|
||||||
|
|
||||||
// TODO allow the user to select multiple versions
|
Ok(())
|
||||||
if let Some(version_id) = mod_info.versions.first() {
|
}
|
||||||
println!("fetching version {}", version_id);
|
|
||||||
|
|
||||||
let version = fetch_mod_version(ctx, version_id).await?;
|
async fn cmd_update(ctx: &AppContext, instance_dir: Option<PathBuf>) -> anyhow::Result<()> {
|
||||||
for file in version.files.iter() {
|
let instance_dir = instance_dir.unwrap_or(std::env::current_dir()?);
|
||||||
download_version_file(ctx, file).await?;
|
let hopfile_path = instance_dir.join("Hopfile.toml");
|
||||||
}
|
let hopfile = std::fs::read_to_string(hopfile_path)?;
|
||||||
}
|
let hopfile: Hopfile = toml::from_str(&hopfile)?;
|
||||||
|
|
||||||
|
println!("hopfile: {:#?}", hopfile);
|
||||||
|
|
||||||
|
// TODO cover range of Minecraft versions? (e.g. 1.18 = 1.18.1, 1.18.2, etc.)
|
||||||
|
let target_versions = Some(vec![hopfile.version.to_owned()]);
|
||||||
|
|
||||||
|
for (name, entry) in hopfile.mods.iter() {
|
||||||
|
let search_args = SearchArgs {
|
||||||
|
package_name: name.to_string(),
|
||||||
|
version: Some(vec![hopfile.version.to_owned()]),
|
||||||
|
};
|
||||||
|
|
||||||
|
let selected = search_and_select(ctx, &search_args).await?;
|
||||||
|
// TODO update Hopfile.toml with specific versions using toml_edit crate
|
||||||
|
download_mods(ctx, Some(&instance_dir), &target_versions, &selected).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -190,6 +313,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let ctx = AppContext { args, config };
|
let ctx = AppContext { args, config };
|
||||||
match ctx.args.to_owned().command {
|
match ctx.args.to_owned().command {
|
||||||
Command::Get(search_args) => cmd_get(&ctx, search_args).await,
|
Command::Get(search_args) => cmd_get(&ctx, search_args).await,
|
||||||
|
Command::Update { instance_dir } => cmd_update(&ctx, instance_dir).await,
|
||||||
_ => unimplemented!("unimplemented subcommand"),
|
_ => unimplemented!("unimplemented subcommand"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue