Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 112 additions & 46 deletions libium/src/add.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<ModIdentifier>,
Expand All @@ -110,22 +116,36 @@ pub async fn add(
filters: Vec<Filter>,
) -> Result<(Vec<String>, 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),
ModIdentifier::ModrinthProject(id) => mr_ids.push(id),
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!(),
}
}

Expand Down Expand Up @@ -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);
Expand All @@ -260,6 +278,7 @@ pub async fn add(
perform_checks,
override_profile,
filters.clone(),
cf_map.get(&project.id),
)
.await
{
Expand Down Expand Up @@ -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
{
Expand All @@ -307,6 +329,7 @@ pub async fn add(
Some(asset_names),
override_profile,
filters.clone(),
gh_map.get(&repo),
)
.await
{
Expand All @@ -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(
Expand All @@ -328,15 +351,20 @@ pub async fn github(
perform_checks: Option<Vec<Metadata>>,
override_profile: bool,
filters: Vec<Filter>,
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);
}

Expand All @@ -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,
Expand All @@ -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<Filter>,
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
Expand All @@ -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()
Expand All @@ -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(_)
)
})
Expand All @@ -429,24 +477,42 @@ 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,
);
Ok(())
}
}

/// 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<Filter>,
serialized: Option<&i32>,
) -> Result<()> {
// Check if project has already been added
if profile.mods.iter().any(|mod_| {
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 10 additions & 5 deletions libium/src/config/structs.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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![
Expand Down Expand Up @@ -139,7 +143,8 @@ pub struct Mod {
#[serde(default)]
pub filters: Vec<Filter>,

/// 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,
Expand Down
17 changes: 13 additions & 4 deletions libium/src/upgrade/mod_downloadable.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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)]
Expand All @@ -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)
Expand Down
Loading