Добавил автопоиск по modrinth api через хэш

This commit is contained in:
vmko 2025-11-16 13:31:20 +03:00
parent 6ef66541b1
commit 6b931bdba2
3 changed files with 1342 additions and 46 deletions

1284
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,3 +12,5 @@ md5 = "0.7"
rayon = "1.10" rayon = "1.10"
log = "0.4" log = "0.4"
env_logger = "0.11" env_logger = "0.11"
reqwest = { version = "0.11", features = ["json", "blocking"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

View File

@ -1,19 +1,24 @@
use log::{error, info, warn}; use log::{error, info, warn};
use md5; use md5;
use rayon::prelude::*; use rayon::prelude::*;
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha1::Sha1; use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512}; use sha2::{Digest, Sha256, Sha512};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{self, BufReader, BufWriter, Read}; use std::io::{self, BufReader, BufWriter, Read};
use std::path::PathBuf; use std::path::PathBuf;
use std::thread;
use std::time::Duration;
const MODS_DIR: &str = "mods"; const MODS_DIR: &str = "mods";
const JSON_FILE: &str = "mods-list.json"; const JSON_FILE: &str = "mods-list.json";
const BUILD_CLIENT_DIR: &str = "build_client"; const BUILD_CLIENT_DIR: &str = "build_client";
const BUILD_SERVER_DIR: &str = "build_server"; const BUILD_SERVER_DIR: &str = "build_server";
const CATEGORIES: [&str; 3] = ["client-only", "server-only", "universal"]; const CATEGORIES: [&str; 3] = ["client-only", "server-only", "universal"];
const MODRINTH_API: &str = "https://api.modrinth.com/v2";
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
struct ModEntry { struct ModEntry {
@ -47,17 +52,14 @@ fn main() {
let mods_dir = PathBuf::from(MODS_DIR); let mods_dir = PathBuf::from(MODS_DIR);
let build_client_dir = PathBuf::from(BUILD_CLIENT_DIR); let build_client_dir = PathBuf::from(BUILD_CLIENT_DIR);
let build_server_dir = PathBuf::from(BUILD_SERVER_DIR); let build_server_dir = PathBuf::from(BUILD_SERVER_DIR);
info!("Запуск менеджера модов"); info!("Запуск менеджера модов");
println!("Выберите действие:"); println!("Выберите действие:");
println!("[1] Обновить моды"); println!("[1] Обновить моды");
println!("[2] Создать сборки"); println!("[2] Создать сборки");
println!("[3] Перевыбрать категории"); println!("[3] Перевыбрать категории");
let mut input = String::new(); let mut input = String::new();
io::stdin().read_line(&mut input).expect("Ошибка ввода"); io::stdin().read_line(&mut input).expect("Ошибка ввода");
let choice = input.trim(); let choice = input.trim();
match choice { match choice {
"1" => update_mods(&mods_dir), "1" => update_mods(&mods_dir),
"2" => { "2" => {
@ -114,7 +116,6 @@ fn compute_hashes_for_file(path: &PathBuf) -> Hashes {
let mut sha1 = Sha1::new(); let mut sha1 = Sha1::new();
let mut md5_ctx = md5::Context::new(); let mut md5_ctx = md5::Context::new();
let mut buffer = [0u8; 8192]; let mut buffer = [0u8; 8192];
loop { loop {
match reader.read(&mut buffer) { match reader.read(&mut buffer) {
Ok(0) => break, Ok(0) => break,
@ -130,7 +131,6 @@ fn compute_hashes_for_file(path: &PathBuf) -> Hashes {
} }
} }
} }
Hashes { Hashes {
sha512: format!("{:x}", sha512.finalize()), sha512: format!("{:x}", sha512.finalize()),
sha256: format!("{:x}", sha256.finalize()), sha256: format!("{:x}", sha256.finalize()),
@ -178,6 +178,41 @@ fn get_jar_files(dir: &PathBuf) -> Vec<PathBuf> {
} }
} }
fn lookup_side(sha1: &str, sha512: &str) -> Option<&'static str> {
let client = Client::new();
for (hash, algo) in [(sha1, "sha1"), (sha512, "sha512")] {
let url = if algo == "sha512" {
format!("{}/version_file/{}?algorithm=sha512", MODRINTH_API, hash)
} else {
format!("{}/version_file/{}", MODRINTH_API, hash)
};
if let Ok(resp) = client.get(&url).send() {
if resp.status() == 200 {
if let Ok(version) = resp.json::<Value>() {
if let Some(project_id) = version["project_id"].as_str() {
if let Ok(project) = client
.get(&format!("{}/project/{}", MODRINTH_API, project_id))
.send()
.and_then(|r| r.json::<Value>())
{
let client_side = project["client_side"].as_str()?;
let server_side = project["server_side"].as_str()?;
return match (client_side, server_side) {
("required", "required") => Some("universal"),
("required", "unsupported") => Some("client-only"),
("unsupported", "required") => Some("server-only"),
_ => None,
};
}
}
}
}
}
thread::sleep(Duration::from_millis(200));
}
None
}
fn process_new_mods( fn process_new_mods(
paths: &[PathBuf], paths: &[PathBuf],
existing_sha512: &HashSet<String>, existing_sha512: &HashSet<String>,
@ -194,7 +229,7 @@ fn process_new_mods(
.to_string_lossy() .to_string_lossy()
.into_owned(); .into_owned();
info!("Новый мод: {}", name); info!("Новый мод: {}", name);
let cat = ask_category(&name); let cat = lookup_side(&hashes.sha1, &hashes.sha512).unwrap_or_else(|| ask_category(&name));
let entry = ModEntry { let entry = ModEntry {
name, name,
sha512: hashes.sha512, sha512: hashes.sha512,
@ -246,18 +281,15 @@ fn update_mods(mods_dir: &PathBuf) {
.chain(&data.universal) .chain(&data.universal)
.map(|e| e.sha512.clone()) .map(|e| e.sha512.clone())
.collect(); .collect();
if !mods_dir.exists() { if !mods_dir.exists() {
error!("Папка {} не найдена!", MODS_DIR); error!("Папка {} не найдена!", MODS_DIR);
return; return;
} }
let all_files = get_jar_files(mods_dir); let all_files = get_jar_files(mods_dir);
let current_names: HashSet<_> = all_files let current_names: HashSet<_> = all_files
.iter() .iter()
.filter_map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned())) .filter_map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.collect(); .collect();
for cat in [ for cat in [
&mut data.client_only, &mut data.client_only,
&mut data.server_only, &mut data.server_only,
@ -272,7 +304,6 @@ fn update_mods(mods_dir: &PathBuf) {
} }
}); });
} }
let new_entries = process_new_mods(&all_files, &existing_sha512); let new_entries = process_new_mods(&all_files, &existing_sha512);
for (entry, cat) in new_entries { for (entry, cat) in new_entries {
match cat { match cat {
@ -282,7 +313,6 @@ fn update_mods(mods_dir: &PathBuf) {
_ => {} _ => {}
} }
} }
if let Err(e) = save_json(&data) { if let Err(e) = save_json(&data) {
error!("Не удалось сохранить JSON: {}", e); error!("Не удалось сохранить JSON: {}", e);
} else { } else {
@ -295,9 +325,7 @@ fn reassign_categories(mods_dir: &PathBuf) {
error!("Папка {} не найдена!", MODS_DIR); error!("Папка {} не найдена!", MODS_DIR);
return; return;
} }
let all_files = get_jar_files(mods_dir); let all_files = get_jar_files(mods_dir);
if PathBuf::from(JSON_FILE).exists() { if PathBuf::from(JSON_FILE).exists() {
if let Err(e) = fs::remove_file(JSON_FILE) { if let Err(e) = fs::remove_file(JSON_FILE) {
warn!("Не удалось удалить старый JSON: {}", e); warn!("Не удалось удалить старый JSON: {}", e);
@ -305,57 +333,43 @@ fn reassign_categories(mods_dir: &PathBuf) {
info!("Старый mods-list.json удалён"); info!("Старый mods-list.json удалён");
} }
} }
let mut data = ModList { let mut data = ModList {
client_only: vec![], client_only: vec![],
server_only: vec![], server_only: vec![],
universal: vec![], universal: vec![],
}; };
let empty_sha512: HashSet<String> = HashSet::new(); let empty_sha512: HashSet<String> = HashSet::new();
let file_hashes = compute_hashes(&all_files);
// 1. Спрашиваем категории для всех модов let mut hash_map: HashMap<String, Hashes> = HashMap::new();
let mut temp_entries: Vec<(String, &'static str)> = vec![]; for (path, hashes) in &file_hashes {
for path in &all_files {
let name = path let name = path
.file_name() .file_name()
.expect("Нет имени файла") .expect("Нет имени файла")
.to_string_lossy() .to_string_lossy()
.into_owned(); .into_owned();
let cat = ask_category(&name); hash_map.insert(name, hashes.clone());
temp_entries.push((name, cat));
} }
// 2. Вычисляем хэши параллельно (в конце)
let file_hashes = compute_hashes(&all_files);
let mut hash_map: HashMap<String, Hashes> = HashMap::new();
for (path, hashes) in file_hashes { for (path, hashes) in file_hashes {
let name = path let name = path
.file_name() .file_name()
.expect("Нет имени файла") .expect("Нет имени файла")
.to_string_lossy() .to_string_lossy()
.into_owned(); .into_owned();
hash_map.insert(name, hashes); let cat = lookup_side(&hashes.sha1, &hashes.sha512).unwrap_or_else(|| ask_category(&name));
} let entry = ModEntry {
name: name.clone(),
// 3. Собираем ModEntry sha512: hashes.sha512,
for (name, cat) in temp_entries { sha256: hashes.sha256,
if let Some(hashes) = hash_map.get(&name) { sha1: hashes.sha1,
let entry = ModEntry { md5: hashes.md5,
name: name.clone(), };
sha512: hashes.sha512.clone(), match cat {
sha256: hashes.sha256.clone(), "client-only" => data.client_only.push(entry),
sha1: hashes.sha1.clone(), "server-only" => data.server_only.push(entry),
md5: hashes.md5.clone(), "universal" => data.universal.push(entry),
}; _ => {}
match cat {
"client-only" => data.client_only.push(entry),
"server-only" => data.server_only.push(entry),
"universal" => data.universal.push(entry),
_ => {}
}
} }
} }
if let Err(e) = save_json(&data) { if let Err(e) = save_json(&data) {
error!("Не удалось сохранить JSON: {}", e); error!("Не удалось сохранить JSON: {}", e);
} else { } else {