Merge pull request #1 from tebibytemedia/get

Get mod files
This commit is contained in:
Emma Tebibyte 2021-11-28 19:43:51 -05:00 committed by GitHub
commit 5c1e8e15cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 181 additions and 5 deletions

View File

@ -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

View File

@ -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(), &params)?;
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(())