diff --git a/libium/src/add.rs b/libium/src/add.rs index e0c1b5d..fdcb4aa 100644 --- a/libium/src/add.rs +++ b/libium/src/add.rs @@ -1,22 +1,25 @@ +use std::{collections::HashMap, str::FromStr}; + +use serde::Deserialize; + use crate::{ config::{ filters::{Filter, ReleaseChannel}, structs::{ModIdentifier, ModLoader, Profile}, }, - iter_ext::IterExt as _, + iter_ext::IterExt, upgrade::{check, Metadata}, CURSEFORGE_API, GITHUB_API, MODRINTH_API, }; -use serde::Deserialize; -use std::{collections::HashMap, str::FromStr}; #[derive(thiserror::Error, Debug)] pub enum Error { #[error( "The developer of this project has denied third party applications from downloading it" )] - /// The user can manually download the mod and place it in the `user` folder of the output directory to mitigate this. - /// However, they will have to manually update the mod. + /// The user can manually download the mod and place it in the `user` + /// folder of the output directory to mitigate this. However, they will + /// have to manually update the mod. DistributionDenied, #[error("The project has already been added")] AlreadyAdded, @@ -96,12 +99,15 @@ pub fn parse_id(id: String) -> ModIdentifier { } } -/// Adds mods from `identifiers`, and returns successful mods with their names, and unsuccessful mods with an error. -/// Currently does not batch requests when adding multiple pinned mods. +/// Adds mods from `identifiers`, and returns successful mods with their names, +/// and unsuccessful mods with an error. Currently does not batch requests when +/// adding multiple pinned mods. /// -/// Classifies the `identifiers` into the appropriate platforms, sends batch requests to get the necessary information, -/// checks details about the projects, and adds them to `profile` if suitable. -/// Performs checks on the mods to see whether they're compatible with the profile if `perform_checks` is true +/// Classifies the `identifiers` into the appropriate platforms, sends batch +/// requests to get the necessary information, checks details about the +/// projects, and adds them to `profile` if suitable. Performs checks on the +/// mods to see whether they're compatible with the profile if `perform_checks` +/// is true pub async fn add( profile: &mut Profile, identifiers: Vec, @@ -110,10 +116,18 @@ pub async fn add( filters: Vec, ) -> Result<(Vec, Vec<(String, Error)>)> { let mut mr_ids = Vec::new(); + let mut mr_map = HashMap::new(); + let mut cf_ids = Vec::new(); + let mut cf_map = HashMap::new(); + let mut gh_ids = Vec::new(); + let mut gh_map = HashMap::new(); + let mut errors = Vec::new(); + let mut success_names = Vec::new(); + for id in identifiers { match id { ModIdentifier::CurseForgeProject(id) => cf_ids.push(id), @@ -121,11 +135,17 @@ pub async fn add( ModIdentifier::GitHubRepository(o, r) => gh_ids.push((o, r)), ModIdentifier::PinnedCurseForgeProject(mod_id, file_id) => { - let project = CURSEFORGE_API.get_mod(mod_id).await?; - let file = CURSEFORGE_API.get_mod_file(mod_id, file_id).await?; + cf_ids.push(mod_id); + cf_map.insert(mod_id, file_id); + } + ModIdentifier::PinnedModrinthProject(project_id, version_id) => { + mr_ids.push(project_id.clone()); + mr_map.insert(project_id, version_id); + } + ModIdentifier::PinnedGitHubRepository((o, r), file_id) => { + gh_ids.push((o.clone(), r.clone())); + gh_map.insert((o, r), file_id); } - ModIdentifier::PinnedModrinthProject(project_id, version_id) => todo!(), - ModIdentifier::PinnedGitHubRepository((owner, repo), asset_id) => todo!(), } } @@ -247,8 +267,6 @@ pub async fn add( .collect_vec() }; - let mut success_names = Vec::new(); - for project in cf_projects { if let Some(i) = cf_ids.iter().position(|&id| id == project.id) { cf_ids.swap_remove(i); @@ -260,6 +278,7 @@ pub async fn add( perform_checks, override_profile, filters.clone(), + cf_map.get(&project.id), ) .await { @@ -287,6 +306,9 @@ pub async fn add( perform_checks, override_profile, filters.clone(), + mr_map + .get(&project.slug) + .or_else(|| mr_map.get(&project.id)), ) .await { @@ -307,6 +329,7 @@ pub async fn add( Some(asset_names), override_profile, filters.clone(), + gh_map.get(&repo), ) .await { @@ -318,8 +341,8 @@ pub async fn add( Ok((success_names, errors)) } -/// Check if the repo of `repo_handler` exists, releases mods, and is compatible with `profile`. -/// If so, add it to the `profile`. +/// Check if the repo of `repo_handler` exists, releases mods, and is compatible +/// with `profile`. If so, add it to the `profile`. /// /// Returns the name of the repository to display to the user pub async fn github( @@ -328,15 +351,20 @@ pub async fn github( perform_checks: Option>, override_profile: bool, filters: Vec, + serialized: Option<&i32>, ) -> Result<()> { // Check if project has already been added - if profile.mods.iter().any(|mod_| { - mod_.name.eq_ignore_ascii_case(id.1.as_ref()) - || matches!( - &mod_.identifier, - ModIdentifier::GitHubRepository(owner, repo) if owner == id.0.as_ref() && repo == id.1.as_ref(), - ) - }) { + if profile + .mods + .iter() + .any(|mod_| { + mod_.name + .eq_ignore_ascii_case(id.1.as_ref()) + || matches!( + &mod_.identifier, + ModIdentifier::GitHubRepository(owner, repo) | ModIdentifier::PinnedGitHubRepository((owner, repo), _) if owner == id.0.as_ref() && repo == id.1.as_ref(), + ) + }) { return Err(Error::AlreadyAdded); } @@ -356,7 +384,15 @@ pub async fn github( // Add it to the profile profile.push_mod( id.1.as_ref().trim().to_string(), - ModIdentifier::GitHubRepository(id.0.to_string(), id.1.to_string()), + serialized.map_or_else( + || ModIdentifier::GitHubRepository(id.0.to_string(), id.1.to_string()), + |file_id| { + ModIdentifier::PinnedGitHubRepository( + (id.0.to_string(), id.1.to_string()), + *file_id, + ) + }, + ), id.1.as_ref().trim().to_string(), override_profile, filters, @@ -367,23 +403,28 @@ pub async fn github( use ferinth::structures::project::{Project, ProjectType}; -/// Check if the project of `project_id` has not already been added, is a mod, and is compatible with `profile`. -/// If so, add it to the `profile`. +/// Check if the project of `project_id` has not already been added, is a mod, +/// and is compatible with `profile`. If so, add it to the `profile`. pub async fn modrinth( project: &Project, profile: &mut Profile, perform_checks: bool, override_profile: bool, filters: Vec, + serialized: Option<&String>, ) -> Result<()> { // Check if project has already been added - if profile.mods.iter().any(|mod_| { - mod_.name.eq_ignore_ascii_case(&project.title) - || matches!( - &mod_.identifier, - ModIdentifier::ModrinthProject(id) if id == &project.id, - ) - }) { + if profile + .mods + .iter() + .any(|mod_| { + mod_.name + .eq_ignore_ascii_case(&project.title) + || matches!( + &mod_.identifier, + ModIdentifier::ModrinthProject(id) | ModIdentifier::PinnedModrinthProject(id, _) if id == &project.id, + ) + }) { Err(Error::AlreadyAdded) // Check if the project is a mod @@ -398,7 +439,9 @@ pub async fn modrinth( filename: "".to_owned(), title: "".to_owned(), description: "".to_owned(), - game_versions: project.game_versions.clone(), + game_versions: project + .game_versions + .clone(), loaders: project .loaders .iter() @@ -408,17 +451,22 @@ pub async fn modrinth( }] .iter(), if override_profile { - profile.filters.clone() + profile.filters + .clone() } else { - [profile.filters.clone(), filters.clone()].concat() + [ + profile.filters + .clone(), + filters.clone(), + ] + .concat() } .iter() .filter(|f| { matches!( f, Filter::GameVersionStrict(_) - | Filter::GameVersionMinor(_) - | Filter::ModLoaderAny(_) + | Filter::GameVersionMinor(_) | Filter::ModLoaderAny(_) | Filter::ModLoaderPrefer(_) ) }) @@ -429,9 +477,26 @@ pub async fn modrinth( } // Add it to the profile profile.push_mod( - project.title.trim().to_owned(), - ModIdentifier::ModrinthProject(project.id.clone()), - project.slug.to_owned(), + project.title + .trim() + .to_owned(), + serialized.map_or_else( + || { + ModIdentifier::ModrinthProject( + project.id + .clone(), + ) + }, + |version_id| { + ModIdentifier::PinnedModrinthProject( + project.id + .clone(), + version_id.clone(), + ) + }, + ), + project.slug + .to_owned(), override_profile, filters, ); @@ -439,14 +504,15 @@ pub async fn modrinth( } } -/// Check if the mod of `project_id` has not already been added, is a mod, and is compatible with `profile`. -/// If so, add it to the `profile`. +/// Check if the mod of `project_id` has not already been added, is a mod, and +/// is compatible with `profile`. If so, add it to the `profile`. pub async fn curseforge( project: &furse::structures::mod_structs::Mod, profile: &mut Profile, perform_checks: bool, override_profile: bool, filters: Vec, + serialized: Option<&i32>, ) -> Result<()> { // Check if project has already been added if profile.mods.iter().any(|mod_| { @@ -510,7 +576,7 @@ pub async fn curseforge( } profile.push_mod( project.name.trim().to_string(), - ModIdentifier::CurseForgeProject(project.id), + serialized.map_or_else(|| ModIdentifier::CurseForgeProject(project.id), |file_id| ModIdentifier::PinnedCurseForgeProject(project.id, *file_id)), project.slug.clone(), override_profile, filters, diff --git a/libium/src/config/structs.rs b/libium/src/config/structs.rs index a964d43..ef9b806 100644 --- a/libium/src/config/structs.rs +++ b/libium/src/config/structs.rs @@ -1,7 +1,9 @@ -use super::filters::Filter; +use std::{path::PathBuf, str::FromStr}; + use derive_more::derive::Display; use serde::{Deserialize, Serialize}; -use std::{path::PathBuf, str::FromStr}; + +use super::filters::Filter; #[derive(Deserialize, Serialize, Debug, Default, Clone)] pub struct Config { @@ -61,7 +63,8 @@ pub struct Profile { } impl Profile { - /// A simple constructor that automatically deals with converting to filters + /// A simple constructor that automatically deals with converting to + /// filters pub fn new( name: String, output_dir: PathBuf, @@ -84,7 +87,8 @@ impl Profile { } } - /// Convert the v4 profile's `game_version` and `mod_loader` fields into filters + /// Convert the v4 profile's `game_version` and `mod_loader` fields into + /// filters pub(crate) fn backwards_compat(&mut self) { if let (Some(version), Some(loader)) = (self.game_version.take(), self.mod_loader.take()) { self.filters = vec![ @@ -139,7 +143,8 @@ pub struct Mod { #[serde(default)] pub filters: Vec, - /// Whether the filters specified above replace or apply with the profile's filters + /// Whether the filters specified above replace or apply with the + /// profile's filters #[serde(skip_serializing_if = "is_false")] #[serde(default)] pub override_filters: bool, diff --git a/libium/src/upgrade/mod_downloadable.rs b/libium/src/upgrade/mod_downloadable.rs index 583e700..eeb46ca 100644 --- a/libium/src/upgrade/mod_downloadable.rs +++ b/libium/src/upgrade/mod_downloadable.rs @@ -1,3 +1,7 @@ +use std::cmp::Reverse; + +use futures_util::TryFutureExt; + use super::{ from_gh_asset, from_gh_releases, from_mr_version, try_from_cf_file, DistributionDeniedError, DownloadData, @@ -10,7 +14,6 @@ use crate::{ iter_ext::IterExt as _, CURSEFORGE_API, GITHUB_API, MODRINTH_API, }; -use std::cmp::Reverse; #[derive(Debug, thiserror::Error)] #[error(transparent)] @@ -37,9 +40,15 @@ impl Mod { ModIdentifier::PinnedCurseForgeProject(mod_id, pin) => { Ok(try_from_cf_file(CURSEFORGE_API.get_mod_file(*mod_id, *pin).await?)?.1) } - ModIdentifier::PinnedModrinthProject(_, pin) => { - Ok(from_mr_version(MODRINTH_API.version_get(pin).await?).1) - } + ModIdentifier::PinnedModrinthProject(project_id, pin) => Ok(from_mr_version( + MODRINTH_API + .version_get(pin) + .or_else(|_| async { + MODRINTH_API.version_get_from_number(project_id, pin).await + }) + .await?, + ) + .1), ModIdentifier::PinnedGitHubRepository((owner, repo), pin) => Ok(from_gh_asset( GITHUB_API .repos(owner, repo) diff --git a/src/main.rs b/src/main.rs index 91ae649..48e215a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,12 @@ mod subcommands; #[cfg(test)] mod tests; +use std::{ + env::{set_var, var_os}, + process::ExitCode, + sync::{LazyLock, OnceLock}, +}; + use anyhow::{anyhow, bail, ensure, Context as _, Result}; use clap::{CommandFactory, Parser}; use cli::{Ferium, ModpackSubCommands, ProfileSubCommands, SubCommands}; @@ -38,11 +44,6 @@ use libium::{ }, iter_ext::IterExt as _, }; -use std::{ - env::{set_var, var_os}, - process::ExitCode, - sync::{LazyLock, OnceLock}, -}; use tokio::sync::Semaphore; const CROSS: &str = "×"; @@ -319,19 +320,25 @@ async fn actual_main(mut cli_app: Ferium) -> Result<()> { println!( "{:20} {}", match &mod_.identifier { - ModIdentifier::CurseForgeProject(id) => + ModIdentifier::CurseForgeProject(id) + | ModIdentifier::PinnedCurseForgeProject(id, _) => format!("{} {:8}", "CF".red(), id.to_string().dimmed()), - ModIdentifier::ModrinthProject(id) => + ModIdentifier::ModrinthProject(id) + | ModIdentifier::PinnedModrinthProject(id, _) => format!("{} {:8}", "MR".green(), id.dimmed()), - ModIdentifier::GitHubRepository(..) => "GH".purple().to_string(), - _ => todo!(), + ModIdentifier::GitHubRepository(..) + | ModIdentifier::PinnedGitHubRepository(..) => + "GH".purple().to_string(), }, match &mod_.identifier { ModIdentifier::ModrinthProject(_) - | ModIdentifier::CurseForgeProject(_) => mod_.name.bold().to_string(), - ModIdentifier::GitHubRepository(owner, repo) => + | ModIdentifier::CurseForgeProject(_) + | ModIdentifier::PinnedCurseForgeProject(..) + | ModIdentifier::PinnedModrinthProject(..) => + mod_.name.bold().to_string(), + ModIdentifier::GitHubRepository(owner, repo) + | ModIdentifier::PinnedGitHubRepository((owner, repo), _) => format!("{}/{}", owner.dimmed(), repo.bold()), - _ => todo!(), }, ); } diff --git a/src/subcommands/list.rs b/src/subcommands/list.rs index 9a6711b..9b932c2 100644 --- a/src/subcommands/list.rs +++ b/src/subcommands/list.rs @@ -1,5 +1,4 @@ -use crate::TICK; -use anyhow::{Context as _, Result}; +use anyhow::{Context, Result}; use colored::Colorize as _; use ferinth::structures::{project::Project, user::TeamMember}; use furse::structures::mod_structs::Mod; @@ -11,6 +10,8 @@ use libium::{ use octocrab::models::{repos::Release, Repository}; use tokio::task::JoinSet; +use crate::TICK; + enum Metadata { CF(Box), MD(Box, Vec), @@ -53,11 +54,15 @@ pub async fn verbose(profile: &mut Profile, markdown: bool) -> Result<()> { let mut tasks = JoinSet::new(); let mut mr_ids = Vec::new(); let mut cf_ids = Vec::new(); + for mod_ in &profile.mods { match mod_.identifier.clone() { - ModIdentifier::CurseForgeProject(project_id) => cf_ids.push(project_id), - ModIdentifier::ModrinthProject(project_id) => mr_ids.push(project_id), - ModIdentifier::GitHubRepository(owner, repo) => { + ModIdentifier::CurseForgeProject(project_id) + | ModIdentifier::PinnedCurseForgeProject(project_id, _) => cf_ids.push(project_id), + ModIdentifier::ModrinthProject(project_id) + | ModIdentifier::PinnedModrinthProject(project_id, _) => mr_ids.push(project_id), + ModIdentifier::GitHubRepository(owner, repo) + | ModIdentifier::PinnedGitHubRepository((owner, repo), _) => { let repo = GITHUB_API.repos(owner, repo); tasks.spawn(async move { Ok::<_, anyhow::Error>(( @@ -66,7 +71,6 @@ pub async fn verbose(profile: &mut Profile, markdown: bool) -> Result<()> { )) }); } - _ => todo!(), } } diff --git a/src/subcommands/remove.rs b/src/subcommands/remove.rs index d148b03..b9c5307 100644 --- a/src/subcommands/remove.rs +++ b/src/subcommands/remove.rs @@ -6,9 +6,11 @@ use libium::{ iter_ext::IterExt as _, }; -/// If `to_remove` is empty, display a list of projects in the profile to select from and remove selected ones +/// If `to_remove` is empty, display a list of projects in the profile to select +/// from and remove selected ones /// -/// Else, search the given strings with the projects' name and IDs and remove them +/// Else, search the given strings with the projects' name and IDs and remove +/// them pub fn remove(profile: &mut Profile, to_remove: Vec) -> Result<()> { let mut indices_to_remove = if to_remove.is_empty() { let mod_info = profile @@ -18,16 +20,24 @@ pub fn remove(profile: &mut Profile, to_remove: Vec) -> Result<()> { format!( "{:11} {}", match &mod_.identifier { - ModIdentifier::CurseForgeProject(id) => format!("CF {:8}", id.to_string()), - ModIdentifier::ModrinthProject(id) => format!("MR {id:8}"), - ModIdentifier::GitHubRepository(..) => "GH".to_string(), - _ => todo!(), + ModIdentifier::PinnedCurseForgeProject(id, _) + | ModIdentifier::CurseForgeProject(id) => + format!("CF {:8}", id.to_string()), + + ModIdentifier::PinnedModrinthProject(id, _) + | ModIdentifier::ModrinthProject(id) => format!("MR {id:8}"), + + ModIdentifier::GitHubRepository(..) + | ModIdentifier::PinnedGitHubRepository(..) => "GH".to_string(), }, match &mod_.identifier { - ModIdentifier::ModrinthProject(_) | ModIdentifier::CurseForgeProject(_) => - mod_.name.clone(), - ModIdentifier::GitHubRepository(owner, repo) => format!("{owner}/{repo}"), - _ => todo!(), + ModIdentifier::ModrinthProject(_) + | ModIdentifier::CurseForgeProject(_) + | ModIdentifier::PinnedCurseForgeProject(..) + | ModIdentifier::PinnedModrinthProject(..) => mod_.name.clone(), + ModIdentifier::GitHubRepository(owner, repo) + | ModIdentifier::PinnedGitHubRepository((owner, repo), _) => + format!("{owner}/{repo}"), }, ) }) @@ -44,12 +54,16 @@ pub fn remove(profile: &mut Profile, to_remove: Vec) -> Result<()> { if let Some(index) = profile.mods.iter().position(|mod_| { mod_.name.eq_ignore_ascii_case(&to_remove) || match &mod_.identifier { - ModIdentifier::CurseForgeProject(id) => id.to_string() == to_remove, - ModIdentifier::ModrinthProject(id) => id == &to_remove, - ModIdentifier::GitHubRepository(owner, name) => { + ModIdentifier::CurseForgeProject(id) + | ModIdentifier::PinnedCurseForgeProject(id, _) => { + id.to_string() == to_remove + } + ModIdentifier::ModrinthProject(id) + | ModIdentifier::PinnedModrinthProject(id, _) => id == &to_remove, + ModIdentifier::GitHubRepository(owner, name) + | ModIdentifier::PinnedGitHubRepository((owner, name), _) => { format!("{owner}/{name}").eq_ignore_ascii_case(&to_remove) } - _ => todo!(), } || mod_ .slug