commit
5c1e8e15cc
|
@ -15,6 +15,10 @@ Long-term/host-dependent:
|
|||
- conflict resolution
|
||||
- dependency resolution
|
||||
- shaderpack and resource pack management
|
||||
- integrate into multimc or theseus
|
||||
- graphical frontend (w/ notifications?)
|
||||
|
||||
[Modrinth REST API docs](https://github.com/modrinth/labrinth/wiki/API-Documentation)
|
||||
|
||||
# File Architecture
|
||||
|
||||
|
|
182
src/main.rs
182
src/main.rs
|
@ -1,5 +1,6 @@
|
|||
use console::style;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
|
@ -59,8 +60,25 @@ impl Default for Upstream {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
struct Options {
|
||||
/// Whether to reverse search results
|
||||
reverse_search: bool,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
reverse_search: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Default)]
|
||||
struct Config {
|
||||
/// General settings
|
||||
options: Options,
|
||||
|
||||
/// Configuration for the upstream Modrinth server
|
||||
upstream: Upstream,
|
||||
}
|
||||
|
@ -75,7 +93,7 @@ struct SearchResponse {
|
|||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ModResult {
|
||||
mod_id: String,
|
||||
mod_id: String, // TODO parse to `local-xxxxx` with regex
|
||||
project_type: Option<String>, // NOTE this isn't in all search results?
|
||||
author: String,
|
||||
title: String,
|
||||
|
@ -120,10 +138,64 @@ impl ModResult {
|
|||
}
|
||||
}
|
||||
|
||||
async fn cmd_get(config: &Config, package_name: String) -> anyhow::Result<()> {
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ModInfo {
|
||||
id: String, // TODO serialize mod id?
|
||||
slug: String,
|
||||
team: String, // TODO serialize team id?
|
||||
title: String,
|
||||
description: String,
|
||||
body: String,
|
||||
published: String, // TODO serialize datetime
|
||||
updated: String, // TODO serialize datetime
|
||||
status: String,
|
||||
// TODO License object
|
||||
// license: String,
|
||||
client_side: String, // TODO serialize as enum
|
||||
server_side: String, // TODO serialize as enum
|
||||
downloads: isize,
|
||||
followers: isize,
|
||||
categories: Vec<String>,
|
||||
versions: Vec<String>,
|
||||
icon_url: Option<String>,
|
||||
issues_url: Option<String>,
|
||||
source_url: Option<String>,
|
||||
wiki_url: Option<String>,
|
||||
discord_url: Option<String>,
|
||||
donation_urls: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ModVersion {
|
||||
id: String, // version id
|
||||
mod_id: String, // mod id
|
||||
author_id: String, // user id
|
||||
// NOTE modrinth docs list this as a String, but is actually a bool?
|
||||
// featured: String, // user id
|
||||
name: String,
|
||||
version_number: String,
|
||||
changelog: Option<String>,
|
||||
changelog_url: Option<String>,
|
||||
date_published: String, // TODO serialize datetime
|
||||
downloads: isize,
|
||||
version_type: String, // TODO {alpha | beta | release}
|
||||
files: Vec<ModVersionFile>,
|
||||
dependencies: Vec<String>, // TODO dependency wrangling, thank you modrinth, very cool
|
||||
game_versions: Vec<String>,
|
||||
loaders: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ModVersionFile {
|
||||
hashes: HashMap<String, String>,
|
||||
url: String,
|
||||
filename: String,
|
||||
}
|
||||
|
||||
async fn search_mods(config: &Config, query: String) -> anyhow::Result<SearchResponse> {
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("https://{}/api/v1/mod", config.upstream.server_address);
|
||||
let params = [("query", package_name.as_str())];
|
||||
let params = [("query", query.as_str())];
|
||||
let url = reqwest::Url::parse_with_params(url.as_str(), ¶ms)?;
|
||||
let response = client
|
||||
.get(url)
|
||||
|
@ -131,8 +203,108 @@ async fn cmd_get(config: &Config, package_name: String) -> anyhow::Result<()> {
|
|||
.await?
|
||||
.json::<SearchResponse>()
|
||||
.await?;
|
||||
for (i, result) in response.hits.iter().enumerate().rev() {
|
||||
result.display(i + 1);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
// TODO config flag to reverse search results order
|
||||
fn display_search_results(config: &Config, response: &SearchResponse) {
|
||||
let iter = response.hits.iter().enumerate();
|
||||
if config.options.reverse_search {
|
||||
for (i, result) in iter.rev() {
|
||||
result.display(i + 1);
|
||||
}
|
||||
} else {
|
||||
for (i, result) in iter {
|
||||
result.display(i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO implement enum for more graceful exiting
|
||||
async fn select_from_results<'a>(
|
||||
_config: &Config,
|
||||
response: &'a SearchResponse,
|
||||
) -> Vec<&'a ModResult> {
|
||||
// TODO actually select with a dialogue
|
||||
match response.hits.first() {
|
||||
Some(first) => vec![first],
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_mod_info(config: &Config, mod_result: &ModResult) -> anyhow::Result<ModInfo> {
|
||||
let client = reqwest::Client::new();
|
||||
let mod_id = &mod_result.mod_id;
|
||||
let mod_id = mod_id[6..].to_owned(); // Remove "local-" prefix
|
||||
let url = format!(
|
||||
"https://{}/api/v1/mod/{}",
|
||||
config.upstream.server_address, mod_id
|
||||
);
|
||||
let response = client.get(url).send().await?;
|
||||
let response = response.json::<ModInfo>().await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn fetch_mod_version(config: &Config, version_id: &String) -> anyhow::Result<ModVersion> {
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!(
|
||||
"https://{}/api/v1/version/{}",
|
||||
config.upstream.server_address, version_id
|
||||
);
|
||||
let response = client.get(url).send().await?;
|
||||
let response = response.json::<ModVersion>().await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn download_version_file(_config: &Config, file: &ModVersionFile) -> anyhow::Result<()> {
|
||||
let client = reqwest::Client::new();
|
||||
let response = client.get(&file.url).send().await?;
|
||||
|
||||
// TODO stream from socket to cache with response.bytes_stream()
|
||||
// TODO check hashes while streaming
|
||||
let filename = &file.filename;
|
||||
println!("downloading to {}...", filename);
|
||||
let mut file = std::fs::File::create(&file.filename)?;
|
||||
let mut content = std::io::Cursor::new(response.bytes().await?);
|
||||
std::io::copy(&mut content, &mut file)?;
|
||||
println!("done downloading.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_get(config: &Config, package_name: String) -> anyhow::Result<()> {
|
||||
let response = search_mods(config, package_name).await?;
|
||||
|
||||
if response.hits.is_empty() {
|
||||
// TODO formatting
|
||||
println!("No results; nothing to do...");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
display_search_results(config, &response);
|
||||
let selected = select_from_results(config, &response).await;
|
||||
|
||||
if selected.is_empty() {
|
||||
// TODO formatting
|
||||
println!("No packages selected; nothing to do...");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for to_get in selected.iter() {
|
||||
let mod_info = fetch_mod_info(config, to_get).await?;
|
||||
println!("mod: {:#?}", mod_info);
|
||||
|
||||
// TODO allow the user to select multiple versions
|
||||
if let Some(version_id) = mod_info.versions.first() {
|
||||
println!("fetching version {}", version_id);
|
||||
|
||||
let version = fetch_mod_version(config, version_id).await?;
|
||||
println!("version: {:#?}", version);
|
||||
|
||||
for file in version.files.iter() {
|
||||
download_version_file(config, file).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
Loading…
Reference in New Issue