Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
Signed-off-by: Lameur <[email protected]>
  • Loading branch information
Lameur committed Feb 18, 2025
1 parent 28999d4 commit b091eb5
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 62 deletions.
5 changes: 4 additions & 1 deletion mod-updater.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ type=jar
path=./test/mohist.jar

# Argument optionnel a passer a un script ou un ficher jar
args=-Xms10G -Xmx30G -XX:+PerfDisableSharedMem
args=-Xms10G -Xmx30G -XX:+PerfDisableSharedMem

# Clé api CurseForge pour les mods de CurseForge
curse-api-key=$2a$10$cKwOkiK6Y2ZHgu5D3KilXeFN9gbZkguTeqzyUW/b8Ap7xvfRu2vYS
243 changes: 182 additions & 61 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,14 @@
/* Copyright (C) 2025 Lameur
This file is part of mods-updater.
mods-updater is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
mods-updater is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License */

use reqwest;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use tokio;
use config::Config;
use std::time::Duration;

#[derive(Debug, Deserialize, Serialize)]
struct ModEntry {
authors: Option<Vec<String>>, // Make `authors` optional
authors: Option<Vec<String>>,
filename: String,
name: String,
url: String,
Expand All @@ -31,18 +18,19 @@ struct ModEntry {
#[derive(Debug, Deserialize)]
struct UpdaterConfig {
#[serde(rename = "type")]
_service_type: Option<String>, // Prefix with an underscore
_path: Option<String>, // Prefix with an underscore
_args: Option<String>, // Prefix with an underscore
_service_type: Option<String>,
_path: Option<String>,
_args: Option<String>,
curse_api_key: Option<String>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config_path = "mod-updater.ini";
let modlist_path = "modlist.json";

// Read and parse the configuration file (intentionally unused for now)
let _config = read_config(config_path)?;
// Read and parse the configuration file
let config = read_config(config_path)?;

// Read and parse the modlist
let modlist_content = fs::read_to_string(modlist_path)?;
Expand All @@ -54,23 +42,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

// If the mod is missing, download it
if !mod_path.exists() {
println!("{} est absent. Téléchargement...", mod_entry.name);
download_mod(&mod_entry).await?;
println!("{} is missing. Downloading...", mod_entry.name);
if let Err(e) = download_mod(&mod_entry, &config.curse_api_key).await {
eprintln!("Failed to download {}: {}", mod_entry.name, e);
}
continue;
}

// Check for updates based on the source (Modrinth only)
match check_for_update(&mod_entry).await {
// Check for updates based on the source (Modrinth or CurseForge)
match check_for_update(&mod_entry, &config.curse_api_key).await {
Ok(Some(new_version)) if new_version != mod_entry.version => {
println!(
"Mise à jour trouvée pour {}: {} -> {}",
"Update found for {}: {} -> {}",
mod_entry.name, mod_entry.version, new_version
);
download_mod(&mod_entry).await?;
if let Err(e) = download_mod(&mod_entry, &config.curse_api_key).await {
eprintln!("Failed to update {}: {}", mod_entry.name, e);
}
}
Ok(_) => println!("{} est à jour.", mod_entry.name),
Ok(_) => println!("{} is up to date.", mod_entry.name),
Err(e) => eprintln!(
"Erreur lors de la vérification de {}: {}",
"Error checking for updates for {}: {}",
mod_entry.name, e
),
}
Expand All @@ -81,7 +73,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

/// Reads and parses the configuration file.
fn read_config(path: &str) -> Result<UpdaterConfig, Box<dyn std::error::Error>> {
let settings = config::Config::builder()
let settings = Config::builder()
.add_source(config::File::with_name(path))
.build()?;

Expand All @@ -91,15 +83,14 @@ fn read_config(path: &str) -> Result<UpdaterConfig, Box<dyn std::error::Error>>
/// Checks for updates for a given mod.
async fn check_for_update(
mod_entry: &ModEntry,
curse_api_key: &Option<String>,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
if is_modrinth_url(&mod_entry.url) {
check_for_update_on_modrinth(&mod_entry).await
} else if is_curseforge_url(&mod_entry.url) {
// Skip CurseForge mods since we don't have an API key
println!("Skipping {} (CurseForge mod, no API key)", mod_entry.name);
Ok(None)
check_for_update_on_curseforge(&mod_entry, curse_api_key).await
} else {
Err("URL non reconnue (doit être CurseForge ou Modrinth)".into())
Err("Unrecognized URL (must be CurseForge or Modrinth)".into())
}
}

Expand All @@ -122,8 +113,16 @@ async fn check_for_update_on_modrinth(
if response.status().is_success() {
let versions: Vec<serde_json::Value> = response.json().await?;

// Sort versions by date (newest first)
let mut sorted_versions = versions;
sorted_versions.sort_by(|a, b| {
let date_a = a["date_published"].as_str().unwrap_or("");
let date_b = b["date_published"].as_str().unwrap_or("");
date_b.cmp(&date_a)
});

// Get the latest version
if let Some(latest_version) = versions.first() {
if let Some(latest_version) = sorted_versions.first() {
let latest_version_number = latest_version["version_number"]
.as_str()
.unwrap_or("")
Expand All @@ -135,16 +134,58 @@ async fn check_for_update_on_modrinth(
Ok(None)
}

/// Downloads a mod from the appropriate source (Modrinth only).
async fn download_mod(mod_entry: &ModEntry) -> Result<(), Box<dyn std::error::Error>> {
/// Checks for updates for a given mod on CurseForge.
async fn check_for_update_on_curseforge(
mod_entry: &ModEntry,
curse_api_key: &Option<String>,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let curseforge_id = extract_curseforge_id(&mod_entry.url)?;

let client = reqwest::Client::new();
let mut request = client
.get(&format!(
"https://api.curse.tools/v1/cf/mods/{}/files/latest",
curseforge_id
));

if let Some(api_key) = curse_api_key {
request = request.header("x-api-key", api_key);
}

let response = request.send().await?;

if response.status().is_success() {
let file_info: serde_json::Value = response.json().await?;

if let Some(latest_version) = file_info["data"]["gameVersions"].as_array() {
if let Some(latest_file) = latest_version.first() {
let latest_version_number = latest_file.as_str().unwrap_or("").to_string();
return Ok(Some(latest_version_number));
}
}
} else {
eprintln!(
"Failed to fetch version info for {}: {} - {}",
mod_entry.name,
response.status(),
response.text().await?
);
}

Ok(None)
}

/// Downloads a mod from the appropriate source (Modrinth or CurseForge).
async fn download_mod(
mod_entry: &ModEntry,
curse_api_key: &Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
if is_modrinth_url(&mod_entry.url) {
download_from_modrinth(&mod_entry).await
} else if is_curseforge_url(&mod_entry.url) {
// Skip CurseForge mods since we don't have an API key
println!("Skipping {} (CurseForge mod, no API key)", mod_entry.name);
Ok(())
download_from_curseforge(&mod_entry, curse_api_key).await
} else {
Err("URL non reconnue (doit être CurseForge ou Modrinth)".into())
Err("Unrecognized URL (must be CurseForge or Modrinth)".into())
}
}

Expand All @@ -165,43 +206,123 @@ async fn download_from_modrinth(mod_entry: &ModEntry) -> Result<(), Box<dyn std:
if response.status().is_success() {
let versions: Vec<serde_json::Value> = response.json().await?;

// Sort versions by date (newest first)
let mut sorted_versions = versions;
sorted_versions.sort_by(|a, b| {
let date_a = a["date_published"].as_str().unwrap_or("");
let date_b = b["date_published"].as_str().unwrap_or("");
date_b.cmp(&date_a)
});

// Get the latest version's download URL
if let Some(latest_version) = versions.first() {
if let Some(latest_version) = sorted_versions.first() {
let download_url = latest_version["files"][0]["url"]
.as_str()
.ok_or("URL de téléchargement introuvable")?;

// Download the mod file
let response = reqwest::get(download_url).await?;
if !response.status().is_success() {
return Err(format!("Échec du téléchargement: {}", response.status()).into());
.ok_or("Download URL not found")?;

// Download the mod file with retries
let mut retries = 3;
while retries > 0 {
let response = reqwest::get(download_url).await?;
if response.status().is_success() {
let bytes = response.bytes().await?;
let mods_dir = PathBuf::from("mods");
if !mods_dir.exists() {
fs::create_dir(&mods_dir)?;
}

let mod_path = mods_dir.join(&mod_entry.filename);
fs::write(&mod_path, bytes)?;

println!("{} downloaded successfully from Modrinth!", mod_entry.name);
return Ok(());
} else {
retries -= 1;
tokio::time::sleep(Duration::from_secs(2)).await; // Wait before retrying
}
}

let bytes = response.bytes().await?;
let mods_dir = PathBuf::from("mods");
if !mods_dir.exists() {
fs::create_dir(&mods_dir)?;
}
return Err("Failed to download after retries".into());
}
}

Err("Failed to fetch version information".into())
}

/// Downloads a mod from CurseForge.
async fn download_from_curseforge(
mod_entry: &ModEntry,
curse_api_key: &Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let curseforge_id = extract_curseforge_id(&mod_entry.url)?;

// Fetch the latest version download URL from CurseForge API
let client = reqwest::Client::new();
let mut request = client
.get(&format!(
"https://api.curse.tools/v1/cf/mods/{}/files/latest",
curseforge_id
));

if let Some(api_key) = curse_api_key {
request = request.header("x-api-key", api_key);
}

let mod_path = mods_dir.join(&mod_entry.filename);
fs::write(&mod_path, bytes)?;
let response = request.send().await?;

println!("{} téléchargé avec succès depuis Modrinth!", mod_entry.name);
return Ok(());
if response.status().is_success() {
let file_info: serde_json::Value = response.json().await?;

// Get the download URL
let download_url = file_info["data"]["downloadUrl"]
.as_str()
.ok_or("Download URL not found")?;

// Download the mod file with retries
let mut retries = 3;
while retries > 0 {
let response = reqwest::get(download_url).await?;
if response.status().is_success() {
let bytes = response.bytes().await?;
let mods_dir = PathBuf::from("mods");
if !mods_dir.exists() {
fs::create_dir(&mods_dir)?;
}

let mod_path = mods_dir.join(&mod_entry.filename);
fs::write(&mod_path, bytes)?;

println!("{} downloaded successfully from CurseForge!", mod_entry.name);
return Ok(());
} else {
retries -= 1;
tokio::time::sleep(Duration::from_secs(2)).await; // Wait before retrying
}
}

return Err("Failed to download after retries".into());
}

Err("Impossible de récupérer les informations de la version".into())
Err("Failed to fetch version information".into())
}

/// Extracts the Modrinth project ID or slug from the URL.
fn extract_modrinth_id(url: &str) -> Result<String, Box<dyn std::error::Error>> {
// Example URL: https://modrinth.com/mod/ai-improvements
let parts: Vec<&str> = url.trim_end_matches('/').split('/').collect();
if parts.len() >= 2 && parts[parts.len() - 2] == "mod" {
Ok(parts[parts.len() - 1].to_string())
} else {
Err("URL de Modrinth invalide".into())
Err("Invalid Modrinth URL".into())
}
}

/// Extracts the CurseForge project ID from the URL.
fn extract_curseforge_id(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let parts: Vec<&str> = url.trim_end_matches('/').split('/').collect();
if parts.len() >= 2 && parts[parts.len() - 2] == "projects" {
Ok(parts[parts.len() - 1].to_string())
} else {
Err("Invalid CurseForge URL".into())
}
}

Expand All @@ -213,4 +334,4 @@ fn is_modrinth_url(url: &str) -> bool {
/// Checks if a URL is a CurseForge URL.
fn is_curseforge_url(url: &str) -> bool {
url.contains("curseforge.com")
}
}

0 comments on commit b091eb5

Please sign in to comment.