feat: unified package categories + curseforge implementation
This commit is contained in:
parent
554e51d543
commit
6841051b7a
25 changed files with 1932 additions and 268 deletions
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
|
@ -27,6 +27,10 @@
|
|||
"rust-analyzer.diagnostics.disabled": [
|
||||
"macro-error" // TODO: Remove this once taurpc upgrades to latest specta
|
||||
],
|
||||
"rust-analyzer.assist.preferSelf": true,
|
||||
"rust-analyzer.procMacro.ignored": {
|
||||
"async-trait": ["async_trait"]
|
||||
},
|
||||
"rust-analyzer.check.command": "check",
|
||||
"rust-analyzer.diagnostics.experimental.enable": false,
|
||||
"rust-analyzer.linkedProjects": [],
|
||||
|
|
|
|||
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -3438,6 +3438,7 @@ dependencies = [
|
|||
"sea-orm",
|
||||
"serde",
|
||||
"serde_json_path_to_error",
|
||||
"serde_repr",
|
||||
"serde_with",
|
||||
"sha1",
|
||||
"sha2",
|
||||
|
|
@ -5034,9 +5035,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.19"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ serde_with = { version = "=3.12.0", features = [
|
|||
"json",
|
||||
"macros"
|
||||
] }
|
||||
serde_repr = { version = "=0.1.20" }
|
||||
toml = { version = "0.8" }
|
||||
url = { version = "2.5" }
|
||||
uuid = { version = "1.16", features = [
|
||||
|
|
|
|||
|
|
@ -14,19 +14,17 @@ authors = { workspace = true }
|
|||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = [ "tauri" ]
|
||||
default = [ ]
|
||||
tauri = [
|
||||
"dep:tauri",
|
||||
"dep:taurpc",
|
||||
"specta",
|
||||
"gui",
|
||||
"onelauncher_macro/tauri",
|
||||
]
|
||||
specta = [
|
||||
"dep:specta",
|
||||
"interpulse/specta",
|
||||
"onelauncher_entity/specta",
|
||||
"onelauncher_macro/specta",
|
||||
]
|
||||
cli = [ "dep:indicatif" ]
|
||||
gui = []
|
||||
|
|
@ -51,6 +49,7 @@ tracing-appender = { workspace = true }
|
|||
serde = { workspace = true }
|
||||
serde_json = { package = "serde_json_path_to_error", path = "../serde_json_path_to_error" }
|
||||
serde_with = { workspace = true }
|
||||
serde_repr = { workspace = true }
|
||||
|
||||
# utils
|
||||
sea-orm = { workspace = true }
|
||||
|
|
|
|||
227
packages/core/src/api/packages/categories.rs
Normal file
227
packages/core/src/api/packages/categories.rs
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
use onelauncher_entity::package::PackageType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::packages::provider::ProviderExt;
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PackageCategories {
|
||||
Mod(Vec<PackageModCategory>),
|
||||
ResourcePack(Vec<PackageResourcePackCategory>),
|
||||
Shader(Vec<PackageShaderCategory>),
|
||||
DataPack(Vec<PackageModCategory>),
|
||||
ModPack(Vec<PackageModPackCategory>),
|
||||
}
|
||||
|
||||
pub trait ToProviderCategory<Out, Provider>
|
||||
where
|
||||
Provider: ProviderExt,
|
||||
{
|
||||
fn as_mod_out(category: &PackageModCategory) -> Out;
|
||||
fn to_mod(value: &Out) -> Option<PackageModCategory>;
|
||||
|
||||
fn as_resource_pack_out(category: &PackageResourcePackCategory) -> Out;
|
||||
fn to_resource_pack(value: &Out) -> Option<PackageResourcePackCategory>;
|
||||
|
||||
fn as_shader_out(category: &PackageShaderCategory) -> Out;
|
||||
fn to_shader(value: &Out) -> Option<PackageShaderCategory>;
|
||||
|
||||
fn as_mod_pack_out(category: &PackageModPackCategory) -> Out;
|
||||
fn to_mod_pack(value: &Out) -> Option<PackageModPackCategory>;
|
||||
|
||||
fn as_data_pack_out(category: &PackageModCategory) -> Out {
|
||||
Self::as_mod_out(category)
|
||||
}
|
||||
|
||||
fn to_data_pack(value: &Out) -> Option<PackageModCategory> {
|
||||
Self::to_mod(value)
|
||||
}
|
||||
|
||||
fn as_out(categories: &PackageCategories) -> Vec<Out> {
|
||||
match categories {
|
||||
PackageCategories::Mod(categories) => {
|
||||
categories.into_iter().map(Self::as_mod_out).collect()
|
||||
}
|
||||
PackageCategories::ResourcePack(categories) => categories
|
||||
.into_iter()
|
||||
.map(Self::as_resource_pack_out)
|
||||
.collect(),
|
||||
PackageCategories::Shader(categories) => {
|
||||
categories.into_iter().map(Self::as_shader_out).collect()
|
||||
}
|
||||
PackageCategories::DataPack(categories) => {
|
||||
categories.into_iter().map(Self::as_data_pack_out).collect()
|
||||
}
|
||||
PackageCategories::ModPack(categories) => {
|
||||
categories.into_iter().map(Self::as_mod_pack_out).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_list(package_type: &PackageType, values: &Vec<Out>) -> PackageCategories {
|
||||
match package_type {
|
||||
&PackageType::Mod => {
|
||||
PackageCategories::Mod(values.iter().filter_map(|v| Self::to_mod(v)).collect())
|
||||
}
|
||||
&PackageType::ResourcePack => PackageCategories::ResourcePack(
|
||||
values
|
||||
.iter()
|
||||
.filter_map(|v| Self::to_resource_pack(v))
|
||||
.collect(),
|
||||
),
|
||||
&PackageType::Shader => PackageCategories::Shader(
|
||||
values.iter().filter_map(|v| Self::to_shader(v)).collect(),
|
||||
),
|
||||
&PackageType::DataPack => PackageCategories::DataPack(
|
||||
values
|
||||
.iter()
|
||||
.filter_map(|v| Self::to_data_pack(v))
|
||||
.collect(),
|
||||
),
|
||||
&PackageType::ModPack => PackageCategories::ModPack(
|
||||
values.iter().filter_map(|v| Self::to_mod_pack(v)).collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! define_category_mapping {
|
||||
(
|
||||
impl $trait_name:ident <$out:ty, $provider:ty> for $struct_name:ty {
|
||||
$(
|
||||
$enum_type:ty as $fn_name:ident => $reverse_fn:ident {
|
||||
$($variant:ident => $value:expr),* $(,)?
|
||||
}
|
||||
)*
|
||||
}
|
||||
) => {
|
||||
impl $trait_name<$out, $provider> for $struct_name {
|
||||
$(
|
||||
fn $fn_name(category: &$enum_type) -> $out {
|
||||
match category {
|
||||
$(
|
||||
<$enum_type>::$variant => $value,
|
||||
)*
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
$(
|
||||
fn $reverse_fn(value: &$out) -> Option<$enum_type> {
|
||||
match value {
|
||||
$(
|
||||
v if v == &$value => Some(<$enum_type>::$variant),
|
||||
)*
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) use define_category_mapping;
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PackageModCategory {
|
||||
Adventure,
|
||||
Library,
|
||||
Equipment,
|
||||
Patches,
|
||||
Cosmetic,
|
||||
Food,
|
||||
Magic,
|
||||
Information,
|
||||
Misc,
|
||||
Performance,
|
||||
Redstone,
|
||||
ServerUtil,
|
||||
Storage,
|
||||
Technology,
|
||||
Farming,
|
||||
Automation,
|
||||
Transport,
|
||||
Utility,
|
||||
QoL,
|
||||
WorldGen,
|
||||
Mobs,
|
||||
Economy,
|
||||
Social,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PackageResourcePackCategory {
|
||||
X8,
|
||||
X16,
|
||||
X32,
|
||||
X48,
|
||||
X64,
|
||||
X128,
|
||||
X256,
|
||||
X512,
|
||||
|
||||
VanillaLike,
|
||||
Utility,
|
||||
Tweaks,
|
||||
Themed,
|
||||
Simplistic,
|
||||
Realistic,
|
||||
Modded,
|
||||
Decoration,
|
||||
Cursed,
|
||||
Combat,
|
||||
|
||||
Audio,
|
||||
Blocks,
|
||||
CoreShaders,
|
||||
Gui,
|
||||
Fonts,
|
||||
Equipment,
|
||||
Environment,
|
||||
Entities,
|
||||
Items,
|
||||
Locale,
|
||||
Models,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PackageShaderCategory {
|
||||
VanillaLike,
|
||||
SemiRealistic,
|
||||
Realistic,
|
||||
Fantasy,
|
||||
Cursed,
|
||||
Cartoon,
|
||||
|
||||
Bloom,
|
||||
Atmosphere,
|
||||
Reflections,
|
||||
Shadows,
|
||||
PBR,
|
||||
PathTracing,
|
||||
Foliage,
|
||||
ColoredLightning,
|
||||
|
||||
Potato,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Ultra,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PackageModPackCategory {
|
||||
Technology,
|
||||
Quests,
|
||||
Optimization,
|
||||
Multiplayer,
|
||||
Magic,
|
||||
LightWeight,
|
||||
Combat,
|
||||
Challenging,
|
||||
Adventure,
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ use onelauncher_entity::package::{PackageType, Provider};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
|
||||
use crate::api::packages::categories::PackageCategories;
|
||||
|
||||
// Divisible by 4 and 3
|
||||
pub const DEFAULT_LIMIT: usize = 24;
|
||||
|
||||
|
|
@ -13,8 +15,8 @@ pub const DEFAULT_LIMIT: usize = 24;
|
|||
pub struct Filters {
|
||||
pub game_versions: Option<Vec<String>>,
|
||||
pub loaders: Option<Vec<GameLoader>>,
|
||||
pub categories: Option<Vec<String>>,
|
||||
pub package_types: Option<Vec<PackageType>>,
|
||||
pub categories: Option<PackageCategories>,
|
||||
pub package_type: Option<PackageType>,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
|
|
@ -53,28 +55,26 @@ pub struct SearchQuery {
|
|||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SearchResult {
|
||||
pub project_id: String,
|
||||
pub project_type: String,
|
||||
pub package_type: PackageType,
|
||||
pub slug: String,
|
||||
pub author: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub categories: Vec<String>,
|
||||
pub display_categories: Vec<String>,
|
||||
pub versions: Vec<String>,
|
||||
pub categories: PackageCategories,
|
||||
pub mc_versions: Vec<String>,
|
||||
pub downloads: usize,
|
||||
pub icon_url: String,
|
||||
pub date_created: DateTime<Utc>,
|
||||
pub date_modified: DateTime<Utc>,
|
||||
pub latest_version: String,
|
||||
pub license: String,
|
||||
pub license: Option<String>,
|
||||
pub client_side: PackageSide,
|
||||
pub server_side: PackageSide,
|
||||
/// List of URLs to images
|
||||
pub gallery: Vec<String>,
|
||||
// pub color: i64,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
|
|
@ -87,7 +87,9 @@ pub struct ManagedPackage {
|
|||
pub package_type: PackageType,
|
||||
pub name: String,
|
||||
pub short_desc: String,
|
||||
pub body: String,
|
||||
pub body: ManagedPackageBody,
|
||||
/// Won't have all versions for some providers, like CurseForge.
|
||||
/// Try making a request to get the versions if needed
|
||||
pub version_ids: Vec<String>,
|
||||
pub mc_versions: Vec<String>,
|
||||
#[serde_as(as = "serde_with::VecSkipError<_>")]
|
||||
|
|
@ -97,15 +99,36 @@ pub struct ManagedPackage {
|
|||
pub updated: DateTime<Utc>,
|
||||
pub client: PackageSide,
|
||||
pub server: PackageSide,
|
||||
pub categories: Vec<String>,
|
||||
pub categories: PackageCategories,
|
||||
pub license: Option<PackageLicense>,
|
||||
pub author: PackageAuthor,
|
||||
pub links: PackageLinks,
|
||||
pub status: PackageStatus,
|
||||
pub downloads: usize,
|
||||
#[serde_as(as = "serde_with::VecSkipError<_>")]
|
||||
pub gallery: Vec<PackageGallery>,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ManagedPackageBody {
|
||||
Url(String),
|
||||
Raw(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ManagedPackageBody {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
ManagedPackageBody::Url(_) => "Url",
|
||||
ManagedPackageBody::Raw(_) => "Raw",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ManagedUser {
|
||||
|
|
@ -173,6 +196,25 @@ pub enum PackageAuthor {
|
|||
Users(Vec<ManagedUser>),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PackageAuthor {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Team { team_id, org_id } => {
|
||||
if let Some(org_id) = org_id {
|
||||
format!("Team({} - {})", team_id, org_id)
|
||||
} else {
|
||||
format!("Team({})", team_id)
|
||||
}
|
||||
}
|
||||
Self::Users(users) => format!("Users({})", users.len()),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use data::{ManagedPackage, ManagedVersion};
|
||||
use onelauncher_entity::icon::Icon;
|
||||
use onelauncher_entity::package::PackageType;
|
||||
use onelauncher_entity::package::{PackageType, Provider};
|
||||
use onelauncher_entity::{clusters, packages};
|
||||
use reqwest::Method;
|
||||
|
||||
use crate::api::packages::data::{ManagedPackageBody, PackageAuthor};
|
||||
use crate::error::{LauncherError, LauncherResult};
|
||||
use crate::store::Dirs;
|
||||
use crate::store::ingress::SubIngress;
|
||||
|
|
@ -11,13 +12,11 @@ use crate::utils::crypto::HashAlgorithm;
|
|||
use crate::utils::io::IOError;
|
||||
use crate::utils::{http, icon, io};
|
||||
|
||||
pub mod categories;
|
||||
pub mod dao;
|
||||
pub mod data;
|
||||
pub mod provider;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[onelauncher_macro::error]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PackageError {
|
||||
|
|
@ -27,6 +26,12 @@ pub enum PackageError {
|
|||
IsModPack,
|
||||
#[error(transparent)]
|
||||
Incompatible(#[from] IncompatiblePackageType),
|
||||
#[error("missing API key for provider '{0}'")]
|
||||
MissingApiKey(Provider),
|
||||
#[error("unsupported package body type '{0}'")]
|
||||
UnsupportedBodyType(ManagedPackageBody),
|
||||
#[error("unsupported author type '{0}'")]
|
||||
UnsupportedAuthorType(PackageAuthor),
|
||||
}
|
||||
|
||||
#[onelauncher_macro::error]
|
||||
|
|
@ -200,5 +205,5 @@ fn join_package_file(
|
|||
version_id: &str,
|
||||
file_name: &str,
|
||||
) -> std::path::PathBuf {
|
||||
path.as_ref().join(format!("{version_id}-{file_name}"))
|
||||
path.as_ref().join(version_id).join(file_name)
|
||||
}
|
||||
|
|
|
|||
103
packages/core/src/api/packages/provider/curseforge/categories.rs
Normal file
103
packages/core/src/api/packages/provider/curseforge/categories.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
use crate::api::packages::categories::{
|
||||
PackageModCategory, PackageModPackCategory, PackageResourcePackCategory, PackageShaderCategory,
|
||||
ToProviderCategory, define_category_mapping,
|
||||
};
|
||||
use crate::api::packages::provider::CurseForgeProviderImpl;
|
||||
|
||||
pub struct CurseForgeCategories;
|
||||
|
||||
define_category_mapping! {
|
||||
impl ToProviderCategory<u32, CurseForgeProviderImpl> for CurseForgeCategories {
|
||||
PackageModCategory as as_mod_out => to_mod {
|
||||
Adventure => 0,
|
||||
Library => 0,
|
||||
Equipment => 0,
|
||||
Patches => 0,
|
||||
Cosmetic => 0,
|
||||
Food => 0,
|
||||
Magic => 0,
|
||||
Information => 0,
|
||||
Misc => 0,
|
||||
Performance => 0,
|
||||
Redstone => 0,
|
||||
ServerUtil => 0,
|
||||
Storage => 0,
|
||||
Technology => 0,
|
||||
Farming => 0,
|
||||
Automation => 0,
|
||||
Transport => 0,
|
||||
Utility => 0,
|
||||
QoL => 0,
|
||||
WorldGen => 0,
|
||||
Mobs => 0,
|
||||
Economy => 0,
|
||||
Social => 0,
|
||||
}
|
||||
|
||||
PackageResourcePackCategory as as_resource_pack_out => to_resource_pack {
|
||||
X8 => 0,
|
||||
X16 => 0,
|
||||
X32 => 0,
|
||||
X48 => 0,
|
||||
X64 => 0,
|
||||
X128 => 0,
|
||||
X256 => 0,
|
||||
X512 => 0,
|
||||
VanillaLike => 0,
|
||||
Utility => 0,
|
||||
Tweaks => 0,
|
||||
Themed => 0,
|
||||
Simplistic => 0,
|
||||
Realistic => 0,
|
||||
Modded => 0,
|
||||
Decoration => 0,
|
||||
Cursed => 0,
|
||||
Combat => 0,
|
||||
Audio => 0,
|
||||
Blocks => 0,
|
||||
CoreShaders => 0,
|
||||
Gui => 0,
|
||||
Fonts => 0,
|
||||
Equipment => 0,
|
||||
Environment => 0,
|
||||
Entities => 0,
|
||||
Items => 0,
|
||||
Locale => 0,
|
||||
Models => 0,
|
||||
}
|
||||
|
||||
PackageShaderCategory as as_shader_out => to_shader {
|
||||
VanillaLike => 0,
|
||||
SemiRealistic => 0,
|
||||
Realistic => 0,
|
||||
Fantasy => 0,
|
||||
Cursed => 0,
|
||||
Cartoon => 0,
|
||||
Bloom => 0,
|
||||
Atmosphere => 0,
|
||||
Reflections => 0,
|
||||
Shadows => 0,
|
||||
PBR => 0,
|
||||
PathTracing => 0,
|
||||
Foliage => 0,
|
||||
ColoredLightning => 0,
|
||||
Potato => 0,
|
||||
Low => 0,
|
||||
Medium => 0,
|
||||
High => 0,
|
||||
Ultra => 0,
|
||||
}
|
||||
|
||||
PackageModPackCategory as as_mod_pack_out => to_mod_pack {
|
||||
Adventure => 0,
|
||||
Technology => 0,
|
||||
Quests => 0,
|
||||
Optimization => 0,
|
||||
Multiplayer => 0,
|
||||
Magic => 0,
|
||||
LightWeight => 0,
|
||||
Combat => 0,
|
||||
Challenging => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
889
packages/core/src/api/packages/provider/curseforge/mod.rs
Normal file
889
packages/core/src/api/packages/provider/curseforge/mod.rs
Normal file
|
|
@ -0,0 +1,889 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
use onelauncher_entity::package::{PackageType, Provider};
|
||||
use reqwest::Method;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use serde_with::serde_as;
|
||||
use url::Url;
|
||||
|
||||
use crate::api::packages::PackageError;
|
||||
use crate::api::packages::categories::ToProviderCategory;
|
||||
use crate::api::packages::data::{
|
||||
ManagedPackage, ManagedPackageBody, ManagedUser, ManagedVersion, ManagedVersionDependency,
|
||||
ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageGallery, PackageLinks,
|
||||
PackageReleaseType, PackageSide, PackageStatus, SearchQuery, SearchResult,
|
||||
};
|
||||
use crate::api::packages::provider::ProviderExt;
|
||||
use crate::api::packages::provider::curseforge::categories::CurseForgeCategories;
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::Core;
|
||||
use crate::utils::http;
|
||||
use crate::utils::pagination::Paginated;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CurseForgeProviderImpl;
|
||||
|
||||
mod categories;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ProviderExt for CurseForgeProviderImpl {
|
||||
async fn search(&self, filter: &SearchQuery) -> LauncherResult<Paginated<SearchResult>> {
|
||||
let results: CFPaginatedData<Vec<CFPackage>> = fetch_url_builder(
|
||||
"/mods/search",
|
||||
Some(|url: &mut Url| {
|
||||
let mut params = url.query_pairs_mut();
|
||||
|
||||
// Always required parameters
|
||||
params.append_pair("gameId", &crate::constants::CURSEFORGE_GAME_ID.to_string());
|
||||
|
||||
let package_type: CFPackageType = filter
|
||||
.filters
|
||||
.as_ref()
|
||||
.and_then(|f| f.package_type.clone())
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
params.append_pair("classId", &(package_type as u32).to_string());
|
||||
|
||||
params.append_pair("pageSize", &filter.limit.unwrap_or(10).to_string());
|
||||
params.append_pair("index", &filter.offset.unwrap_or(0).to_string());
|
||||
params.append_pair("sortField", &(SearchSort::Popularity as u8).to_string());
|
||||
params.append_pair("sortOrder", "desc");
|
||||
|
||||
// Optional: search query
|
||||
if let Some(query) = &filter.query {
|
||||
params.append_pair("searchFilter", query);
|
||||
}
|
||||
|
||||
if let Some(filters) = &filter.filters {
|
||||
if let Some(game_versions) = &filters.game_versions {
|
||||
// Need to create a list e.g. ["1.16.5", "1.17.1"]
|
||||
let game_versions = format!(
|
||||
"[{}]",
|
||||
game_versions
|
||||
.iter()
|
||||
.map(|v| format!("\"{}\"", v))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
);
|
||||
|
||||
params.append_pair("gameVersions", game_versions.as_str());
|
||||
}
|
||||
|
||||
if let Some(loaders) = &filters.loaders {
|
||||
// Need to create a list e.g. [1, 2, 3]
|
||||
let loaders = format!(
|
||||
"[{}]",
|
||||
loaders
|
||||
.iter()
|
||||
.map(|l| (CFLoader::from(l.clone()) as u32).to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
);
|
||||
|
||||
params.append_pair("modLoaderTypes", loaders.as_str());
|
||||
}
|
||||
|
||||
if let Some(categories) = &filters.categories {
|
||||
let categories = CurseForgeCategories::as_out(categories);
|
||||
|
||||
if !categories.is_empty() {
|
||||
// Need to create a list e.g. [6, 12, 4471]
|
||||
let categories = format!(
|
||||
"[{}]",
|
||||
categories
|
||||
.iter()
|
||||
.map(u32::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
);
|
||||
|
||||
params.append_pair("categoryIds", categories.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Paginated {
|
||||
total: results.pagination.total_count,
|
||||
offset: results.pagination.index,
|
||||
limit: results.pagination.page_size,
|
||||
items: results.data.into_iter().map(SearchResult::from).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get(&self, slug: &str) -> LauncherResult<ManagedPackage> {
|
||||
Ok(fetch::<CFData<CFPackage>>(&format!("/mods/{slug}"))
|
||||
.await?
|
||||
.data
|
||||
.into())
|
||||
}
|
||||
|
||||
async fn get_multiple(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedPackage>> {
|
||||
let body = serde_json::json!({
|
||||
"modIds": slugs.iter().filter_map(|s| s.parse::<u32>().ok()).collect::<Vec<u32>>(),
|
||||
});
|
||||
|
||||
let results: CFData<Vec<CFPackage>> = fetch_advanced(
|
||||
Method::POST,
|
||||
"/mods",
|
||||
None::<fn(&mut Url)>,
|
||||
None,
|
||||
Some(body),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(results.data.into_iter().map(ManagedPackage::from).collect())
|
||||
}
|
||||
|
||||
async fn get_versions_by_hashes(
|
||||
&self,
|
||||
hashes: &[String],
|
||||
) -> LauncherResult<HashMap<String, ManagedVersion>> {
|
||||
let body = serde_json::json!({
|
||||
"fingerprints": hashes.into_iter().filter_map(|hash| hash.parse::<u32>().ok()).collect::<Vec<u32>>()
|
||||
});
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FingerprintMatch {
|
||||
// id: u32,
|
||||
file: CFModFile,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Response {
|
||||
exact_matches: Vec<FingerprintMatch>,
|
||||
// exact_fingerprints: Vec<u32>
|
||||
}
|
||||
|
||||
let results: CFData<Response> = fetch_advanced(
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/mods/fingerprints/{}",
|
||||
crate::constants::CURSEFORGE_GAME_ID
|
||||
),
|
||||
None::<fn(&mut Url)>,
|
||||
None,
|
||||
Some(body),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(results
|
||||
.data
|
||||
.exact_matches
|
||||
.into_iter()
|
||||
.map(|file| {
|
||||
(
|
||||
file.file.file_fingerprint.to_string(),
|
||||
ManagedVersion::from(file.file),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<Option<ManagedVersion>> {
|
||||
let results = self.get_versions_by_hashes(&[hash.to_string()]).await?;
|
||||
Ok(results.values().next().cloned())
|
||||
}
|
||||
|
||||
async fn get_users_from_author(
|
||||
&self,
|
||||
author: PackageAuthor,
|
||||
) -> LauncherResult<Vec<ManagedUser>> {
|
||||
match author {
|
||||
PackageAuthor::Users(users) => Ok(users),
|
||||
_ => Err(PackageError::UnsupportedAuthorType(author).into()),
|
||||
}
|
||||
}
|
||||
|
||||
// async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
|
||||
// todo!("CurseForge get users not implemented yet")
|
||||
// }
|
||||
|
||||
// async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
|
||||
// todo!("CurseForge get user not implemented yet")
|
||||
// }
|
||||
|
||||
async fn get_versions_paginated(
|
||||
&self,
|
||||
slug: &str,
|
||||
mc_version: Option<String>,
|
||||
loaders: Option<GameLoader>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> LauncherResult<Paginated<ManagedVersion>> {
|
||||
let results: CFPaginatedData<Vec<CFModFile>> = fetch_url_builder(
|
||||
&format!("/mods/{slug}/files"),
|
||||
Some(|url: &mut Url| {
|
||||
let mut params = url.query_pairs_mut();
|
||||
|
||||
if let Some(mc_version) = mc_version {
|
||||
params.append_pair("gameVersion", &mc_version);
|
||||
}
|
||||
|
||||
if let Some(loader) = loaders {
|
||||
params
|
||||
.append_pair("modLoaderType", &(CFLoader::from(loader) as u8).to_string());
|
||||
}
|
||||
|
||||
let page = offset / limit;
|
||||
|
||||
params.append_pair("pageSize", &limit.to_string());
|
||||
params.append_pair("index", &page.to_string());
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Paginated {
|
||||
total: results.pagination.total_count,
|
||||
offset: results.pagination.index,
|
||||
limit: results.pagination.page_size,
|
||||
items: results.data.into_iter().map(ManagedVersion::from).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_versions(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedVersion>> {
|
||||
let json_body = serde_json::json!({
|
||||
"fileIds": slugs.iter().filter_map(|v| v.parse::<u32>().ok()).collect::<Vec<u32>>()
|
||||
});
|
||||
|
||||
let results: CFData<Vec<CFModFile>> = fetch_advanced(
|
||||
Method::POST,
|
||||
"/mods/files",
|
||||
None::<fn(&mut Url)>,
|
||||
None,
|
||||
Some(json_body),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(results.data.into_iter().map(ManagedVersion::from).collect())
|
||||
}
|
||||
|
||||
async fn get_body(&self, body: &ManagedPackageBody) -> LauncherResult<String> {
|
||||
match body {
|
||||
ManagedPackageBody::Raw(raw) => Ok(raw.clone()),
|
||||
ManagedPackageBody::Url(url) => {
|
||||
let body: String = fetch(url).await?;
|
||||
Ok(body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: fetch helpers
|
||||
|
||||
async fn fetch_advanced<T: DeserializeOwned, F: FnOnce(&mut Url)>(
|
||||
method: Method,
|
||||
url: &str,
|
||||
url_builder: Option<F>,
|
||||
headers: Option<HashMap<&str, &str>>,
|
||||
json_body: Option<serde_json::Value>,
|
||||
) -> LauncherResult<T> {
|
||||
let core = Core::get();
|
||||
let key = core
|
||||
.curseforge_api_key
|
||||
.as_ref()
|
||||
.ok_or(PackageError::MissingApiKey(
|
||||
onelauncher_entity::package::Provider::CurseForge,
|
||||
))?;
|
||||
|
||||
let mut headers = headers.unwrap_or(HashMap::<&str, &str>::new());
|
||||
headers.insert("x-api-key", key);
|
||||
|
||||
let url = &mut Url::parse(format!("{}{}", crate::constants::CURSEFORGE_API_URL, url).as_str())?;
|
||||
|
||||
if let Some(url_builder) = url_builder {
|
||||
url_builder(url);
|
||||
}
|
||||
|
||||
http::fetch_json_advanced(method, url.as_str(), json_body, Some(headers), None, None).await
|
||||
}
|
||||
|
||||
pub async fn fetch_url_builder<T: DeserializeOwned, F: FnOnce(&mut Url)>(
|
||||
url: &str,
|
||||
url_builder: Option<F>,
|
||||
) -> LauncherResult<T> {
|
||||
fetch_advanced::<T, F>(Method::GET, url, url_builder, None, None).await
|
||||
}
|
||||
|
||||
async fn fetch<T: DeserializeOwned>(url: &str) -> LauncherResult<T> {
|
||||
fetch_url_builder::<T, fn(&mut Url)>(url, None).await
|
||||
}
|
||||
|
||||
// MARK: data structs
|
||||
/// Mapping of Curseforge "classes" to their respective category ids
|
||||
#[derive(Debug, Default, Serialize_repr, Deserialize_repr)]
|
||||
#[repr(u32)]
|
||||
pub enum CFPackageType {
|
||||
#[default]
|
||||
Mods = 6,
|
||||
ModPack = 4471,
|
||||
DataPack = 6945,
|
||||
ResourcePack = 12,
|
||||
ShaderPack = 6552,
|
||||
}
|
||||
|
||||
impl From<CFPackageType> for PackageType {
|
||||
fn from(package_type: CFPackageType) -> Self {
|
||||
match package_type {
|
||||
CFPackageType::Mods => Self::Mod,
|
||||
CFPackageType::ModPack => Self::ModPack,
|
||||
CFPackageType::DataPack => Self::DataPack,
|
||||
CFPackageType::ResourcePack => Self::ResourcePack,
|
||||
CFPackageType::ShaderPack => Self::Shader,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PackageType> for CFPackageType {
|
||||
fn from(package_type: PackageType) -> Self {
|
||||
match package_type {
|
||||
PackageType::Mod => Self::Mods,
|
||||
PackageType::ModPack => Self::ModPack,
|
||||
PackageType::DataPack => Self::DataPack,
|
||||
PackageType::ResourcePack => Self::ResourcePack,
|
||||
PackageType::Shader => Self::ShaderPack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mapping of Curseforge supported loaders to their respective id
|
||||
#[derive(Debug, Default, Serialize_repr, Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum CFLoader {
|
||||
#[default]
|
||||
Any = 0,
|
||||
Forge = 1,
|
||||
Cauldron = 2,
|
||||
LiteLoader = 3,
|
||||
Fabric = 4,
|
||||
Quilt = 5,
|
||||
NeoForge = 6,
|
||||
}
|
||||
|
||||
impl From<String> for CFLoader {
|
||||
fn from(loader: String) -> Self {
|
||||
match loader.to_lowercase().as_str() {
|
||||
"forge" => CFLoader::Forge,
|
||||
"cauldron" => CFLoader::Cauldron,
|
||||
"liteloader" => CFLoader::LiteLoader,
|
||||
"fabric" => CFLoader::Fabric,
|
||||
"quilt" => CFLoader::Quilt,
|
||||
"neoforge" => CFLoader::NeoForge,
|
||||
_ => CFLoader::Any,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GameLoader> for CFLoader {
|
||||
fn from(loader: GameLoader) -> Self {
|
||||
match loader {
|
||||
GameLoader::Forge => CFLoader::Forge,
|
||||
GameLoader::Fabric | GameLoader::LegacyFabric => CFLoader::Fabric,
|
||||
GameLoader::Quilt => CFLoader::Quilt,
|
||||
GameLoader::NeoForge => CFLoader::NeoForge,
|
||||
_ => CFLoader::Any,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CFLoader> for GameLoader {
|
||||
fn from(loader: CFLoader) -> Self {
|
||||
match loader {
|
||||
CFLoader::Forge => Self::Forge,
|
||||
CFLoader::Fabric => Self::Fabric,
|
||||
CFLoader::Quilt => Self::Quilt,
|
||||
CFLoader::NeoForge => Self::NeoForge,
|
||||
_ => GameLoader::Vanilla,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde_as]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CFPackage {
|
||||
pub id: usize,
|
||||
pub game_id: usize,
|
||||
#[serde(alias = "classId")]
|
||||
pub package_type: CFPackageType,
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
pub links: CFLinks,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "serde_with::VecSkipError<_>")]
|
||||
pub social_links: Vec<CFSocialLink>,
|
||||
pub summary: String,
|
||||
pub status: CFStatus,
|
||||
pub download_count: usize,
|
||||
#[serde(default)]
|
||||
pub is_featured: bool,
|
||||
pub categories: Vec<Category>,
|
||||
pub authors: Vec<CFAuthor>,
|
||||
pub logo: Option<CFModAsset>,
|
||||
pub screenshots: Vec<CFModAsset>,
|
||||
#[serde_as(as = "serde_with::VecSkipError<_>")]
|
||||
pub latest_files_indexes: Vec<CFLatestFileIndex>,
|
||||
pub date_created: DateTime<Utc>,
|
||||
pub date_modified: DateTime<Utc>,
|
||||
pub date_released: DateTime<Utc>,
|
||||
pub rating: Option<f32>,
|
||||
}
|
||||
|
||||
impl From<CFPackage> for ManagedPackage {
|
||||
fn from(value: CFPackage) -> Self {
|
||||
let package_type: PackageType = value.package_type.into();
|
||||
|
||||
let mut loaders: Vec<GameLoader> = Vec::new();
|
||||
let mut mc_versions: Vec<String> = Vec::new();
|
||||
let mut latest_version_ids: Vec<String> = Vec::new();
|
||||
|
||||
for index in value.latest_files_indexes {
|
||||
mc_versions.push(index.game_version);
|
||||
latest_version_ids.push(index.file_id.to_string());
|
||||
|
||||
if let Some(loader) = index
|
||||
.mod_loader
|
||||
.and_then(|loader| GameLoader::try_from(loader).ok())
|
||||
{
|
||||
loaders.push(loader);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
provider: Provider::CurseForge,
|
||||
id: value.id.to_string(),
|
||||
slug: value.slug,
|
||||
name: value.name,
|
||||
short_desc: value.summary,
|
||||
body: ManagedPackageBody::Url(format!("/v1/mods/{}/description", value.id)),
|
||||
version_ids: latest_version_ids,
|
||||
mc_versions,
|
||||
loaders,
|
||||
icon_url: value.logo.map(|logo| logo.url),
|
||||
created: value.date_created,
|
||||
updated: value.date_modified,
|
||||
client: PackageSide::Unknown, // TODO: determine client side
|
||||
server: PackageSide::Unknown, // TODO: determine server side
|
||||
categories: CurseForgeCategories::to_list(
|
||||
&package_type,
|
||||
&value.categories.into_iter().map(|c| c.id).collect(),
|
||||
),
|
||||
package_type,
|
||||
license: None,
|
||||
author: PackageAuthor::Users(
|
||||
value
|
||||
.authors
|
||||
.into_iter()
|
||||
.map(|author| ManagedUser {
|
||||
id: author.id.to_string(),
|
||||
username: author.name,
|
||||
url: Some(author.url),
|
||||
is_organization_user: false,
|
||||
avatar_url: author.avatar_url,
|
||||
bio: None,
|
||||
role: None,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
links: PackageLinks {
|
||||
source: value.links.source_url,
|
||||
issues: value.links.issues_url,
|
||||
wiki: value.links.wiki_url,
|
||||
donation: None, // we love curseforge
|
||||
|
||||
discord: value
|
||||
.social_links
|
||||
.iter()
|
||||
.find(|l| l.link_type == CFSocialLinkType::Discord)
|
||||
.map(|l| l.url.clone()),
|
||||
website: value
|
||||
.social_links
|
||||
.iter()
|
||||
.find(|l| l.link_type == CFSocialLinkType::Website)
|
||||
.map(|l| l.url.clone()),
|
||||
|
||||
..Default::default()
|
||||
},
|
||||
status: value.status.into(),
|
||||
downloads: value.download_count,
|
||||
gallery: value.screenshots.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CFPackage> for SearchResult {
|
||||
fn from(value: CFPackage) -> Self {
|
||||
let mc_versions: Vec<String> = value
|
||||
.latest_files_indexes
|
||||
.into_iter()
|
||||
.map(|index| index.game_version)
|
||||
.collect();
|
||||
|
||||
let package_type: PackageType = value.package_type.into();
|
||||
|
||||
Self {
|
||||
title: value.name,
|
||||
slug: value.slug,
|
||||
project_id: value.id.to_string(),
|
||||
author: value
|
||||
.authors
|
||||
.first()
|
||||
.and_then(|a| Some(a.name.clone()))
|
||||
.unwrap_or(String::from("Unknown")),
|
||||
description: value.summary,
|
||||
client_side: PackageSide::Unknown,
|
||||
server_side: PackageSide::Unknown,
|
||||
downloads: value.download_count,
|
||||
icon_url: value.logo.map_or(String::new(), |l| l.url),
|
||||
categories: CurseForgeCategories::to_list(
|
||||
&package_type,
|
||||
&value.categories.into_iter().map(|c| c.id).collect(),
|
||||
),
|
||||
package_type,
|
||||
mc_versions,
|
||||
date_created: value.date_created,
|
||||
date_modified: value.date_modified,
|
||||
gallery: value
|
||||
.screenshots
|
||||
.into_iter()
|
||||
.map(|asset| asset.url)
|
||||
.collect(),
|
||||
latest_version: "".to_string(), // TODO: figure out how to get this
|
||||
license: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CFLatestFileIndex {
|
||||
pub game_version: String,
|
||||
pub file_id: usize,
|
||||
#[serde(rename = "filename")]
|
||||
pub file_name: String,
|
||||
// pub release_type: ModFileReleaseType,
|
||||
pub mod_loader: Option<CFLoader>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
|
||||
#[repr(u64)]
|
||||
pub enum CFStatus {
|
||||
#[default]
|
||||
Unknown = 0,
|
||||
New = 1,
|
||||
Approved = 4,
|
||||
Rejected = 5,
|
||||
Deleted = 9,
|
||||
}
|
||||
|
||||
impl From<CFStatus> for PackageStatus {
|
||||
fn from(status: CFStatus) -> Self {
|
||||
match status {
|
||||
CFStatus::Unknown => Self::Active,
|
||||
CFStatus::New => Self::Active,
|
||||
CFStatus::Approved => Self::Active,
|
||||
CFStatus::Rejected => Self::Abandoned,
|
||||
CFStatus::Deleted => Self::Abandoned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CFLinks {
|
||||
pub website_url: Option<String>,
|
||||
pub wiki_url: Option<String>,
|
||||
pub issues_url: Option<String>,
|
||||
pub source_url: Option<String>,
|
||||
}
|
||||
|
||||
// undocumented ??
|
||||
#[derive(Default, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
|
||||
#[repr(u64)]
|
||||
pub enum CFSocialLinkType {
|
||||
None = 0,
|
||||
Mastodon = 1,
|
||||
Discord = 2,
|
||||
Website = 3,
|
||||
Facebook = 4,
|
||||
Twitter = 5,
|
||||
Instagram = 6,
|
||||
Patreon = 7,
|
||||
Twitch = 8,
|
||||
Reddit = 9,
|
||||
Youtube = 10,
|
||||
TikTok = 11,
|
||||
Pinterest = 12,
|
||||
Github = 13,
|
||||
BlueSky = 14,
|
||||
|
||||
#[default]
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CFSocialLink {
|
||||
#[serde(rename = "type")]
|
||||
pub link_type: CFSocialLinkType,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Category {
|
||||
pub id: u32,
|
||||
// pub game_id: u32,
|
||||
// pub name: String,
|
||||
pub slug: String,
|
||||
// pub url: String,
|
||||
// pub icon_url: String,
|
||||
// pub date_modified: String,
|
||||
// pub parent_category_id: u32,
|
||||
// pub display_index: u32,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CFAuthor {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CFModAsset {
|
||||
pub id: u32,
|
||||
pub mod_id: u32,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub thumbnail_url: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl From<CFModAsset> for PackageGallery {
|
||||
fn from(screenshot: CFModAsset) -> Self {
|
||||
Self {
|
||||
url: screenshot.url,
|
||||
thumbnail_url: screenshot.thumbnail_url,
|
||||
title: Some(screenshot.title),
|
||||
description: Some(screenshot.description),
|
||||
featured: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize_repr, Deserialize_repr, Clone, Debug)]
|
||||
#[repr(u8)]
|
||||
pub enum ModFileReleaseType {
|
||||
#[default]
|
||||
Release = 1,
|
||||
Beta = 2,
|
||||
Alpha = 3,
|
||||
}
|
||||
|
||||
impl From<ModFileReleaseType> for PackageReleaseType {
|
||||
fn from(value: ModFileReleaseType) -> Self {
|
||||
match value {
|
||||
ModFileReleaseType::Release => Self::Release,
|
||||
ModFileReleaseType::Beta => Self::Beta,
|
||||
ModFileReleaseType::Alpha => Self::Alpha,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CFModFile {
|
||||
pub id: u32,
|
||||
pub game_id: u32,
|
||||
pub mod_id: u32,
|
||||
#[serde(default)]
|
||||
pub is_available: bool,
|
||||
pub display_name: String,
|
||||
pub file_name: String,
|
||||
pub release_type: ModFileReleaseType,
|
||||
// pub file_status: u32,
|
||||
pub hashes: Vec<Hash>,
|
||||
pub file_date: DateTime<Utc>,
|
||||
pub file_length: usize,
|
||||
pub download_count: usize,
|
||||
pub file_size_on_disk: Option<usize>,
|
||||
pub download_url: String,
|
||||
pub game_versions: Vec<String>,
|
||||
pub sortable_game_versions: Vec<SortableGameVersion>,
|
||||
pub dependencies: Vec<CFDependency>,
|
||||
// pub expose_as_alternative: bool,
|
||||
// pub parent_project_file_id: u32,
|
||||
// pub alternate_file_id: u32,
|
||||
// pub is_server_pack: bool,
|
||||
// pub server_pack_file_id: u32,
|
||||
// pub is_early_access_content: bool,
|
||||
// pub early_access_end_date: String,
|
||||
pub file_fingerprint: u32,
|
||||
// pub modules: Vec<Module>,
|
||||
}
|
||||
|
||||
impl From<CFModFile> for ManagedVersion {
|
||||
fn from(value: CFModFile) -> ManagedVersion {
|
||||
let mut hashes = HashMap::new();
|
||||
|
||||
for hash in value.hashes {
|
||||
hashes.insert(hash.algo.name(), hash.value);
|
||||
}
|
||||
|
||||
let mut mc_versions = Vec::new();
|
||||
let mut loaders = Vec::new();
|
||||
|
||||
for version in value.sortable_game_versions {
|
||||
if version.game_version.is_empty() {
|
||||
// This is most likely a loader
|
||||
loaders.push(CFLoader::from(version.game_version_name));
|
||||
} else {
|
||||
mc_versions.push(version.game_version);
|
||||
}
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
files.push(ManagedVersionFile {
|
||||
url: value.download_url,
|
||||
file_name: value.file_name.clone(),
|
||||
primary: true,
|
||||
size: value.file_size_on_disk.unwrap_or(value.file_length),
|
||||
sha1: hashes.get("sha1").cloned().unwrap_or_default(),
|
||||
});
|
||||
|
||||
ManagedVersion {
|
||||
project_id: value.mod_id.to_string(),
|
||||
version_id: value.id.to_string(),
|
||||
display_name: value.display_name.clone(),
|
||||
loaders: loaders.into_iter().map(Into::into).collect(),
|
||||
mc_versions,
|
||||
changelog: None,
|
||||
display_version: value.file_name,
|
||||
release_type: value.release_type.into(),
|
||||
downloads: value.download_count,
|
||||
published: value.file_date,
|
||||
dependencies: value.dependencies.into_iter().map(Into::into).collect(),
|
||||
files,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum HashAlgorithm {
|
||||
Sha1 = 1,
|
||||
MD5 = 2,
|
||||
}
|
||||
|
||||
impl HashAlgorithm {
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
Self::Sha1 => "sha1",
|
||||
Self::MD5 => "md5",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Hash {
|
||||
pub value: String,
|
||||
pub algo: HashAlgorithm,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SortableGameVersion {
|
||||
pub game_version_name: String,
|
||||
pub game_version_padded: String,
|
||||
pub game_version: String,
|
||||
pub game_version_release_date: String,
|
||||
pub game_version_type_id: u32,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CFDependency {
|
||||
pub mod_id: usize,
|
||||
pub relation_type: CFRelationType,
|
||||
}
|
||||
|
||||
impl From<CFDependency> for ManagedVersionDependency {
|
||||
fn from(value: CFDependency) -> Self {
|
||||
Self {
|
||||
project_id: Some(value.mod_id.to_string()),
|
||||
dependency_type: value.relation_type.into(),
|
||||
file_name: None,
|
||||
version_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize_repr, Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum CFRelationType {
|
||||
Embedded = 1,
|
||||
#[default]
|
||||
Optional = 2,
|
||||
Required = 3,
|
||||
Tool = 4,
|
||||
Incompatible = 5,
|
||||
Include = 6,
|
||||
}
|
||||
|
||||
impl From<CFRelationType> for PackageDependencyType {
|
||||
fn from(value: CFRelationType) -> Self {
|
||||
match value {
|
||||
CFRelationType::Embedded => Self::Embedded,
|
||||
CFRelationType::Optional => Self::Optional,
|
||||
CFRelationType::Required => Self::Required,
|
||||
CFRelationType::Tool => Self::Optional,
|
||||
CFRelationType::Incompatible => Self::Incompatible,
|
||||
CFRelationType::Include => Self::Embedded,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.curseforge.com/rest-api/?shell#tocS_ModsSearchSortField
|
||||
#[allow(dead_code)]
|
||||
enum SearchSort {
|
||||
Featured = 1,
|
||||
Popularity = 2,
|
||||
LastUpdated = 3,
|
||||
Name = 4,
|
||||
Author = 5,
|
||||
TotalDownloads = 6,
|
||||
Rating = 12,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
struct CFData<T> {
|
||||
data: T,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
struct CFPaginatedData<T> {
|
||||
pagination: Pagination,
|
||||
data: T,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Pagination {
|
||||
pub index: usize,
|
||||
pub page_size: usize,
|
||||
pub total_count: usize,
|
||||
}
|
||||
|
|
@ -1,15 +1,25 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
use onelauncher_entity::package::Provider;
|
||||
|
||||
use crate::api::packages::data::SearchResult;
|
||||
use crate::api::packages::PackageError;
|
||||
use crate::api::packages::data::{ManagedPackageBody, SearchResult};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::utils::pagination::Paginated;
|
||||
|
||||
use super::data::{ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery};
|
||||
|
||||
mod modrinth;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
mod curseforge;
|
||||
mod modrinth;
|
||||
// mod skyclient;
|
||||
|
||||
pub use curseforge::CurseForgeProviderImpl;
|
||||
pub use modrinth::ModrinthProviderImpl;
|
||||
// pub use skyclient::SkyClientProviderImpl;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ProviderExt {
|
||||
|
|
@ -19,8 +29,8 @@ pub trait ProviderExt {
|
|||
async fn get_versions_by_hashes(
|
||||
&self,
|
||||
hashes: &[String],
|
||||
) -> LauncherResult<Vec<ManagedVersion>>;
|
||||
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<ManagedVersion>;
|
||||
) -> LauncherResult<HashMap<String, ManagedVersion>>;
|
||||
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<Option<ManagedVersion>>;
|
||||
|
||||
// async fn get_org_projects(&self, slug: &str) -> LauncherResult<Vec<ManagedPackage>>;
|
||||
|
||||
|
|
@ -28,107 +38,96 @@ pub trait ProviderExt {
|
|||
&self,
|
||||
author: PackageAuthor,
|
||||
) -> LauncherResult<Vec<ManagedUser>>;
|
||||
async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>>;
|
||||
async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser>;
|
||||
// async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>>;
|
||||
// async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser>;
|
||||
|
||||
async fn get_versions_paginated(
|
||||
&self,
|
||||
slug: &str,
|
||||
mc_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<GameLoader>>,
|
||||
mc_version: Option<String>,
|
||||
loader: Option<GameLoader>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> LauncherResult<Paginated<ManagedVersion>>;
|
||||
|
||||
async fn get_versions(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedVersion>>;
|
||||
|
||||
async fn get_body(&self, body: &ManagedPackageBody) -> LauncherResult<String> {
|
||||
match body {
|
||||
ManagedPackageBody::Raw(raw) => Ok(raw.clone()),
|
||||
_ => Err(PackageError::UnsupportedBodyType(body.clone()).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleans up code duplication by delegating method calls to the appropriate provider implementation.
|
||||
macro_rules! delegate {
|
||||
($self:expr, $method:ident($($arg:expr),*)) => {
|
||||
match $self {
|
||||
Provider::Modrinth => ModrinthProviderImpl.$method($($arg),*).await,
|
||||
Provider::CurseForge => CurseForgeProviderImpl.$method($($arg),*).await,
|
||||
// Provider::SkyClient => SkyClientProviderImpl.$method($($arg),*).await,
|
||||
_ => todo!("skyclient not implement")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Wrapper around all supported providers into a single enum.
|
||||
#[async_trait::async_trait]
|
||||
impl ProviderExt for Provider {
|
||||
async fn search(&self, query: &SearchQuery) -> LauncherResult<Paginated<SearchResult>> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.search(query).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
delegate!(self, search(query))
|
||||
}
|
||||
|
||||
async fn get(&self, slug: &str) -> LauncherResult<ManagedPackage> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get(slug).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
delegate!(self, get(slug))
|
||||
}
|
||||
|
||||
async fn get_multiple(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedPackage>> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get_multiple(slugs).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
delegate!(self, get_multiple(slugs))
|
||||
}
|
||||
|
||||
async fn get_versions_by_hashes(
|
||||
&self,
|
||||
hashes: &[String],
|
||||
) -> LauncherResult<Vec<ManagedVersion>> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get_versions_by_hashes(hashes).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
) -> LauncherResult<HashMap<String, ManagedVersion>> {
|
||||
delegate!(self, get_versions_by_hashes(hashes))
|
||||
}
|
||||
|
||||
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<ManagedVersion> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get_version_by_hash(hash).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<Option<ManagedVersion>> {
|
||||
delegate!(self, get_version_by_hash(hash))
|
||||
}
|
||||
|
||||
async fn get_users_from_author(
|
||||
&self,
|
||||
author: PackageAuthor,
|
||||
) -> LauncherResult<Vec<ManagedUser>> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get_users_from_author(author).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
delegate!(self, get_users_from_author(author))
|
||||
}
|
||||
|
||||
async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get_users(slugs).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
}
|
||||
// async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
|
||||
// delegate!(self, get_users(slugs))
|
||||
// }
|
||||
|
||||
async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get_user(slug).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
}
|
||||
// async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
|
||||
// delegate!(self, get_user(slug))
|
||||
// }
|
||||
|
||||
async fn get_versions_paginated(
|
||||
&self,
|
||||
slug: &str,
|
||||
mc_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<GameLoader>>,
|
||||
mc_version: Option<String>,
|
||||
loader: Option<GameLoader>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> LauncherResult<Paginated<ManagedVersion>> {
|
||||
match self {
|
||||
Self::Modrinth => {
|
||||
ModrinthProviderImpl
|
||||
.get_versions_paginated(slug, mc_versions, loaders, offset, limit)
|
||||
.await
|
||||
}
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
delegate!(
|
||||
self,
|
||||
get_versions_paginated(slug, mc_version, loader, offset, limit)
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_versions(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedVersion>> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get_versions(slugs).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
delegate!(self, get_versions(slugs))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
103
packages/core/src/api/packages/provider/modrinth/categories.rs
Normal file
103
packages/core/src/api/packages/provider/modrinth/categories.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
use crate::api::packages::categories::{
|
||||
PackageModCategory, PackageModPackCategory, PackageResourcePackCategory, PackageShaderCategory,
|
||||
ToProviderCategory, define_category_mapping,
|
||||
};
|
||||
use crate::api::packages::provider::ModrinthProviderImpl;
|
||||
|
||||
pub struct ModrinthCategories;
|
||||
|
||||
define_category_mapping! {
|
||||
impl ToProviderCategory<String, ModrinthProviderImpl> for ModrinthCategories {
|
||||
PackageModCategory as as_mod_out => to_mod {
|
||||
Adventure => String::from("adventure"),
|
||||
Library => String::from("library"),
|
||||
Equipment => String::from("equipment"),
|
||||
Patches => String::from("optimization"),
|
||||
Cosmetic => String::from("decoration"),
|
||||
Food => String::from("food"),
|
||||
Magic => String::from("magic"),
|
||||
Information => String::from("utility"),
|
||||
Misc => String::from("utility"),
|
||||
Performance => String::from("optimization"),
|
||||
Redstone => String::from("technology"),
|
||||
ServerUtil => String::from("management"),
|
||||
Storage => String::from("storage"),
|
||||
Technology => String::from("technology"),
|
||||
Farming => String::from("food"),
|
||||
Automation => String::from("technology"),
|
||||
Transport => String::from("transportation"),
|
||||
Utility => String::from("utility"),
|
||||
QoL => String::from("utility"),
|
||||
WorldGen => String::from("worldgen"),
|
||||
Mobs => String::from("mobs"),
|
||||
Economy => String::from("economy"),
|
||||
Social => String::from("social"),
|
||||
}
|
||||
|
||||
PackageResourcePackCategory as as_resource_pack_out => to_resource_pack {
|
||||
X8 => String::from("8x-"),
|
||||
X16 => String::from("16x"),
|
||||
X32 => String::from("32x"),
|
||||
X48 => String::from("48x"),
|
||||
X64 => String::from("64x"),
|
||||
X128 => String::from("128x"),
|
||||
X256 => String::from("256x"),
|
||||
X512 => String::from("512x+"),
|
||||
VanillaLike => String::from("vanilla-like"),
|
||||
Utility => String::from("utility"),
|
||||
Tweaks => String::from("tweaks"),
|
||||
Themed => String::from("themed"),
|
||||
Simplistic => String::from("simplistic"),
|
||||
Realistic => String::from("realistic"),
|
||||
Modded => String::from("modded"),
|
||||
Decoration => String::from("decoration"),
|
||||
Cursed => String::from("cursed"),
|
||||
Combat => String::from("combat"),
|
||||
Audio => String::from("audio"),
|
||||
Blocks => String::from("blocks"),
|
||||
CoreShaders => String::from("core-shaders"),
|
||||
Gui => String::from("gui"),
|
||||
Fonts => String::from("fonts"),
|
||||
Equipment => String::from("equipment"),
|
||||
Environment => String::from("environment"),
|
||||
Entities => String::from("entities"),
|
||||
Items => String::from("items"),
|
||||
Locale => String::from("locale"),
|
||||
Models => String::from("models"),
|
||||
}
|
||||
|
||||
PackageShaderCategory as as_shader_out => to_shader {
|
||||
VanillaLike => String::from("vanilla-like"),
|
||||
SemiRealistic => String::from("semi-realistic"),
|
||||
Realistic => String::from("realistic"),
|
||||
Fantasy => String::from("fantasy"),
|
||||
Cursed => String::from("cursed"),
|
||||
Cartoon => String::from("cartoon"),
|
||||
Bloom => String::from("bloom"),
|
||||
Atmosphere => String::from("atmosphere"),
|
||||
Reflections => String::from("reflections"),
|
||||
Shadows => String::from("shadows"),
|
||||
PBR => String::from("pbr"),
|
||||
PathTracing => String::from("path-tracing"),
|
||||
Foliage => String::from("foliage"),
|
||||
ColoredLightning => String::from("colored-lightning"),
|
||||
Potato => String::from("potato"),
|
||||
Low => String::from("low"),
|
||||
Medium => String::from("medium"),
|
||||
High => String::from("high"),
|
||||
Ultra => String::from("screenshot"),
|
||||
}
|
||||
|
||||
PackageModPackCategory as as_mod_pack_out => to_mod_pack {
|
||||
Adventure => String::from("adventure"),
|
||||
Technology => String::from("technology"),
|
||||
Quests => String::from("quests"),
|
||||
Optimization => String::from("optimization"),
|
||||
Multiplayer => String::from("multiplayer"),
|
||||
Magic => String::from("magic"),
|
||||
LightWeight => String::from("lightweight"),
|
||||
Combat => String::from("combat"),
|
||||
Challenging => String::from("challenging"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use super::ProviderExt;
|
||||
use crate::api::packages::categories::ToProviderCategory;
|
||||
use crate::api::packages::data::{
|
||||
DEFAULT_LIMIT, ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency,
|
||||
ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageGallery,
|
||||
PackageLicense, PackageLinks, PackageReleaseType, PackageSide, PackageStatus, SearchQuery,
|
||||
SearchResult,
|
||||
DEFAULT_LIMIT, ManagedPackage, ManagedPackageBody, ManagedUser, ManagedVersion,
|
||||
ManagedVersionDependency, ManagedVersionFile, PackageAuthor, PackageDependencyType,
|
||||
PackageDonationUrl, PackageGallery, PackageLicense, PackageLinks, PackageReleaseType,
|
||||
PackageSide, PackageStatus, SearchQuery, SearchResult,
|
||||
};
|
||||
use crate::api::packages::provider::modrinth::categories::ModrinthCategories;
|
||||
use crate::error::LauncherResult;
|
||||
use crate::utils::http;
|
||||
use crate::utils::pagination::Paginated;
|
||||
|
|
@ -16,6 +20,8 @@ use serde::Deserialize;
|
|||
use serde_with::serde_as;
|
||||
use url::Url;
|
||||
|
||||
mod categories;
|
||||
|
||||
macro_rules! url {
|
||||
($($arg:tt)*) => {
|
||||
format!("{}/v2{}", crate::constants::MODRINTH_API_URL, format!($($arg)*)).as_str()
|
||||
|
|
@ -31,13 +37,6 @@ macro_rules! url_v3 {
|
|||
#[derive(Default)]
|
||||
pub struct ModrinthProviderImpl;
|
||||
|
||||
impl ModrinthProviderImpl {
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ProviderExt for ModrinthProviderImpl {
|
||||
async fn search(&self, query: &SearchQuery) -> LauncherResult<Paginated<SearchResult>> {
|
||||
|
|
@ -54,39 +53,33 @@ impl ProviderExt for ModrinthProviderImpl {
|
|||
let mut builder = FacetBuilder::builder();
|
||||
|
||||
if let Some(categories) = &filters.categories {
|
||||
for category in categories {
|
||||
builder.and(Facet(
|
||||
"categories".to_string(),
|
||||
FacetOperation::Eq,
|
||||
category.to_string(),
|
||||
));
|
||||
for category in ModrinthCategories::as_out(categories) {
|
||||
builder.and(Facet::new("categories", FacetOperation::Eq, category));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mc_versions) = &filters.game_versions {
|
||||
for mc_version in mc_versions {
|
||||
builder.and(Facet(
|
||||
"versions".to_string(),
|
||||
builder.and(Facet::new(
|
||||
"versions",
|
||||
FacetOperation::Eq,
|
||||
mc_version.to_string(),
|
||||
mc_version.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(package_types) = &filters.package_types {
|
||||
for package_type in package_types {
|
||||
builder.and(Facet(
|
||||
"project_type".to_string(),
|
||||
FacetOperation::Eq,
|
||||
package_type_to_string(package_type),
|
||||
));
|
||||
}
|
||||
if let Some(package_type) = &filters.package_type {
|
||||
builder.and(Facet::new(
|
||||
"project_type",
|
||||
FacetOperation::Eq,
|
||||
package_type.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(loaders) = &filters.loaders {
|
||||
for loader in loaders {
|
||||
builder.and(Facet(
|
||||
"categories".to_string(),
|
||||
builder.and(Facet::new(
|
||||
"categories",
|
||||
FacetOperation::Eq,
|
||||
loader.to_string(),
|
||||
));
|
||||
|
|
@ -152,33 +145,34 @@ impl ProviderExt for ModrinthProviderImpl {
|
|||
async fn get_versions_by_hashes(
|
||||
&self,
|
||||
hashes: &[String],
|
||||
) -> LauncherResult<Vec<ManagedVersion>> {
|
||||
) -> LauncherResult<HashMap<String, ManagedVersion>> {
|
||||
let body = serde_json::json!({
|
||||
"hashes": hashes,
|
||||
"algorithm": "sha1"
|
||||
});
|
||||
|
||||
Ok(http::fetch_json::<Vec<ModrinthVersion>>(
|
||||
let results = http::fetch_json::<HashMap<String, ModrinthVersion>>(
|
||||
Method::POST,
|
||||
url!("/version_files"),
|
||||
Some(body),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect())
|
||||
.await?;
|
||||
|
||||
Ok(results.into_iter().map(|(k, v)| (k, v.into())).collect())
|
||||
}
|
||||
|
||||
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<ManagedVersion> {
|
||||
Ok(http::fetch_json::<ModrinthVersion>(
|
||||
Method::GET,
|
||||
url!("/version_file/{hash}"),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.into())
|
||||
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<Option<ManagedVersion>> {
|
||||
Ok(Some(
|
||||
http::fetch_json::<ModrinthVersion>(
|
||||
Method::GET,
|
||||
url!("/version_file/{hash}"),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.into(),
|
||||
))
|
||||
}
|
||||
|
||||
// async fn get_org_projects(&self, slug: &str) -> LauncherResult<Vec<ManagedPackage>> {
|
||||
|
|
@ -252,45 +246,45 @@ impl ProviderExt for ModrinthProviderImpl {
|
|||
Ok(users)
|
||||
}
|
||||
|
||||
async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
|
||||
Ok(http::fetch_json::<Vec<ModrinthUser>>(
|
||||
Method::GET,
|
||||
url!("/users?ids=[{}]", &serde_json::to_string(&slugs)?),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect())
|
||||
}
|
||||
// async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
|
||||
// Ok(http::fetch_json::<Vec<ModrinthUser>>(
|
||||
// Method::GET,
|
||||
// url!("/users?ids=[{}]", &serde_json::to_string(&slugs)?),
|
||||
// None,
|
||||
// None,
|
||||
// )
|
||||
// .await?
|
||||
// .into_iter()
|
||||
// .map(Into::into)
|
||||
// .collect())
|
||||
// }
|
||||
|
||||
async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
|
||||
Ok(
|
||||
http::fetch_json::<ModrinthUser>(Method::GET, url!("/user/{slug}"), None, None)
|
||||
.await?
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
// async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
|
||||
// Ok(
|
||||
// http::fetch_json::<ModrinthUser>(Method::GET, url!("/user/{slug}"), None, None)
|
||||
// .await?
|
||||
// .into(),
|
||||
// )
|
||||
// }
|
||||
|
||||
async fn get_versions_paginated(
|
||||
&self,
|
||||
slug: &str,
|
||||
mc_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<GameLoader>>,
|
||||
mc_version: Option<String>,
|
||||
loader: Option<GameLoader>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> LauncherResult<Paginated<ManagedVersion>> {
|
||||
let mut url = Url::parse(url!("/project/{slug}/version"))?;
|
||||
|
||||
if let Some(mc_versions) = mc_versions {
|
||||
if let Some(mc_version) = mc_version {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("game_versions", &serde_json::to_string(&mc_versions)?);
|
||||
.append_pair("game_versions", &format!("[\"{}\"]", mc_version));
|
||||
}
|
||||
|
||||
if let Some(loaders) = loaders {
|
||||
if let Some(loader) = loader {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("loaders", &serde_json::to_string(&loaders)?);
|
||||
.append_pair("loaders", &format!("[\"{}\"]", loader.to_string()));
|
||||
}
|
||||
|
||||
// url.query_pairs_mut()
|
||||
|
|
@ -335,15 +329,50 @@ impl ProviderExt for ModrinthProviderImpl {
|
|||
}
|
||||
}
|
||||
|
||||
fn package_type_to_string(package_type: &PackageType) -> String {
|
||||
match package_type {
|
||||
PackageType::Mod => "mod",
|
||||
PackageType::ResourcePack => "resourcepack",
|
||||
PackageType::Shader => "shader",
|
||||
PackageType::DataPack => "datapack",
|
||||
PackageType::ModPack => "modpack",
|
||||
#[derive(Deserialize)]
|
||||
struct ModrinthSearchResult {
|
||||
pub project_id: String,
|
||||
pub project_type: PackageType,
|
||||
pub slug: String,
|
||||
pub author: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub categories: Vec<String>,
|
||||
pub mc_versions: Vec<String>,
|
||||
pub downloads: usize,
|
||||
pub icon_url: String,
|
||||
pub date_created: DateTime<Utc>,
|
||||
pub date_modified: DateTime<Utc>,
|
||||
pub latest_version: String,
|
||||
pub license: Option<String>,
|
||||
pub client_side: PackageSide,
|
||||
pub server_side: PackageSide,
|
||||
/// List of URLs to images
|
||||
pub gallery: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<ModrinthSearchResult> for SearchResult {
|
||||
fn from(value: ModrinthSearchResult) -> Self {
|
||||
Self {
|
||||
categories: ModrinthCategories::to_list(&value.project_type, &value.categories),
|
||||
package_type: value.project_type,
|
||||
project_id: value.project_id,
|
||||
slug: value.slug,
|
||||
author: value.author,
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
mc_versions: value.mc_versions,
|
||||
downloads: value.downloads,
|
||||
icon_url: value.icon_url,
|
||||
date_created: value.date_created,
|
||||
date_modified: value.date_modified,
|
||||
latest_version: value.latest_version,
|
||||
license: value.license,
|
||||
client_side: value.client_side,
|
||||
server_side: value.server_side,
|
||||
gallery: value.gallery,
|
||||
}
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
|
@ -407,10 +436,9 @@ impl From<ModrinthPackage> for ManagedPackage {
|
|||
provider: Provider::Modrinth,
|
||||
id: value.id,
|
||||
slug: value.slug,
|
||||
package_type: value.project_type,
|
||||
name: value.title,
|
||||
short_desc: value.description,
|
||||
body: value.body,
|
||||
body: ManagedPackageBody::Raw(value.body),
|
||||
version_ids: value.versions,
|
||||
mc_versions: value.game_versions,
|
||||
loaders: value.loaders,
|
||||
|
|
@ -419,7 +447,8 @@ impl From<ModrinthPackage> for ManagedPackage {
|
|||
updated: value.updated,
|
||||
client: value.client_side,
|
||||
server: value.server_side,
|
||||
categories: value.categories,
|
||||
categories: ModrinthCategories::to_list(&value.project_type, &value.categories),
|
||||
package_type: value.project_type,
|
||||
license: value.license,
|
||||
author: PackageAuthor::Team {
|
||||
team_id: value.team,
|
||||
|
|
@ -444,20 +473,25 @@ impl From<ModrinthPackage> for ManagedPackage {
|
|||
}
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Deserialize)]
|
||||
struct ModrinthVersion {
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
pub changelog: Option<String>,
|
||||
#[serde_as(as = "serde_with::VecSkipError<_>")]
|
||||
pub dependencies: Vec<ModrinthDependency>,
|
||||
#[serde_as(as = "serde_with::VecSkipError<_>")]
|
||||
pub game_versions: Vec<String>,
|
||||
pub version_type: PackageReleaseType,
|
||||
#[serde_as(as = "serde_with::VecSkipError<_>")]
|
||||
pub loaders: Vec<GameLoader>,
|
||||
// pub featured: bool,
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub date_published: DateTime<Utc>,
|
||||
pub downloads: usize,
|
||||
#[serde_as(as = "serde_with::VecSkipError<_>")]
|
||||
pub files: Vec<ModrinthFile>,
|
||||
}
|
||||
|
||||
|
|
@ -611,6 +645,12 @@ impl std::fmt::Display for FacetOperation {
|
|||
|
||||
struct Facet(pub String, pub FacetOperation, pub String);
|
||||
|
||||
impl Facet {
|
||||
pub fn new(key: &str, operation: FacetOperation, value: String) -> Self {
|
||||
Self(key.to_string(), operation, value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Facet {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(format!("{}{}{}", self.0, self.1, self.2).as_str())
|
||||
69
packages/core/src/api/packages/provider/skyclient.rs
Normal file
69
packages/core/src/api/packages/provider/skyclient.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
|
||||
use crate::api::packages::data::{
|
||||
ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery, SearchResult,
|
||||
};
|
||||
use crate::api::packages::provider::ProviderExt;
|
||||
use crate::error::LauncherResult;
|
||||
use crate::utils::pagination::Paginated;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SkyClientProviderImpl;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ProviderExt for SkyClientProviderImpl {
|
||||
async fn search(&self, query: &SearchQuery) -> LauncherResult<Paginated<SearchResult>> {
|
||||
todo!("SkyClient search not implemented yet")
|
||||
}
|
||||
|
||||
async fn get(&self, slug: &str) -> LauncherResult<ManagedPackage> {
|
||||
todo!("SkyClient get package not implemented yet")
|
||||
}
|
||||
|
||||
async fn get_multiple(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedPackage>> {
|
||||
todo!("SkyClient get multiple packages not implemented yet")
|
||||
}
|
||||
|
||||
async fn get_versions_by_hashes(
|
||||
&self,
|
||||
hashes: &[String],
|
||||
) -> LauncherResult<HashMap<String, ManagedVersion>> {
|
||||
todo!("SkyClient get versions by hashes not implemented yet")
|
||||
}
|
||||
|
||||
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<Option<ManagedVersion>> {
|
||||
todo!("SkyClient get version by hash not implemented yet")
|
||||
}
|
||||
|
||||
async fn get_users_from_author(
|
||||
&self,
|
||||
author: PackageAuthor,
|
||||
) -> LauncherResult<Vec<ManagedUser>> {
|
||||
todo!("SkyClient get users from author not implemented yet")
|
||||
}
|
||||
|
||||
// async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
|
||||
// todo!("SkyClient get users not implemented yet")
|
||||
// }
|
||||
|
||||
// async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
|
||||
// todo!("SkyClient get user not implemented yet")
|
||||
// }
|
||||
|
||||
async fn get_versions_paginated(
|
||||
&self,
|
||||
slug: &str,
|
||||
mc_version: Option<String>,
|
||||
loader: Option<GameLoader>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> LauncherResult<Paginated<ManagedVersion>> {
|
||||
todo!("SkyClient get versions paginated not implemented yet")
|
||||
}
|
||||
|
||||
async fn get_versions(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedVersion>> {
|
||||
todo!("SkyClient get versions not implemented yet")
|
||||
}
|
||||
}
|
||||
211
packages/core/src/api/packages/provider/tests.rs
Normal file
211
packages/core/src/api/packages/provider/tests.rs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
use std::sync::LazyLock;
|
||||
|
||||
use onelauncher_entity::package::{PackageType, Provider};
|
||||
|
||||
use crate::api::packages;
|
||||
use crate::api::packages::provider::ProviderExt;
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::{Core, CoreOptions, Dirs};
|
||||
|
||||
macro_rules! init {
|
||||
() => {{
|
||||
Core::initialize(CoreOptions::default()).await?;
|
||||
if Core::get().curseforge_api_key.is_none() {
|
||||
eprintln!("CURSEFORGE_API_KEY is not set, skipping tests that require it.");
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
slug: &'static str,
|
||||
id: &'static str,
|
||||
version_id: &'static str,
|
||||
package_type: PackageType,
|
||||
}
|
||||
|
||||
static DATA: LazyLock<Vec<(Provider, Vec<Entry>)>> = LazyLock::new(|| {
|
||||
vec![
|
||||
(
|
||||
Provider::CurseForge,
|
||||
vec![
|
||||
Entry {
|
||||
slug: "oneconfig",
|
||||
id: "1148006",
|
||||
version_id: "5938441",
|
||||
package_type: PackageType::Mod,
|
||||
},
|
||||
Entry {
|
||||
slug: "complementary-reimagined",
|
||||
id: "627557",
|
||||
version_id: "6515577",
|
||||
package_type: PackageType::Shader,
|
||||
},
|
||||
Entry {
|
||||
slug: "tschipcrafts-dynamic-lights",
|
||||
id: "831385",
|
||||
version_id: "6725286",
|
||||
package_type: PackageType::DataPack,
|
||||
},
|
||||
Entry {
|
||||
slug: "fresh-animations",
|
||||
id: "453763",
|
||||
version_id: "6528594",
|
||||
package_type: PackageType::ResourcePack,
|
||||
},
|
||||
Entry {
|
||||
slug: "all-the-mods-10",
|
||||
id: "925200",
|
||||
version_id: "6826503",
|
||||
package_type: PackageType::ModPack,
|
||||
},
|
||||
],
|
||||
),
|
||||
(
|
||||
Provider::Modrinth,
|
||||
vec![
|
||||
Entry {
|
||||
slug: "oneconfig",
|
||||
id: "AibBIVmj",
|
||||
version_id: "YofF8Rpk",
|
||||
package_type: PackageType::Mod,
|
||||
},
|
||||
Entry {
|
||||
slug: "complementary-reimagined",
|
||||
id: "HVnmMxH1",
|
||||
version_id: "sAAjYvFB",
|
||||
package_type: PackageType::Shader,
|
||||
},
|
||||
Entry {
|
||||
slug: "veinminer",
|
||||
id: "OhduvhIc",
|
||||
version_id: "gZ4v72II",
|
||||
package_type: PackageType::Mod,
|
||||
},
|
||||
Entry {
|
||||
slug: "fresh-animations",
|
||||
id: "50dA9Sha",
|
||||
version_id: "9LtDLleW",
|
||||
package_type: PackageType::ResourcePack,
|
||||
},
|
||||
Entry {
|
||||
slug: "fabulously-optimized",
|
||||
id: "1KVo5zza",
|
||||
version_id: "U9MwqSo1",
|
||||
package_type: PackageType::ModPack,
|
||||
},
|
||||
],
|
||||
),
|
||||
]
|
||||
});
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_get() -> LauncherResult<()> {
|
||||
let cf = init!();
|
||||
|
||||
for (provider, entries) in DATA.iter() {
|
||||
if provider == &Provider::CurseForge && !cf {
|
||||
continue;
|
||||
}
|
||||
|
||||
for entry in entries {
|
||||
let res = provider.get(entry.id).await?;
|
||||
|
||||
assert_eq!(res.slug, entry.slug);
|
||||
assert_eq!(res.id, entry.id);
|
||||
assert_eq!(res.package_type, entry.package_type);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_get_multiple() -> LauncherResult<()> {
|
||||
let cf = init!();
|
||||
|
||||
for (provider, entries) in DATA.iter() {
|
||||
if provider == &Provider::CurseForge && !cf {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ids = entries
|
||||
.iter()
|
||||
.map(|e| e.id.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
let res = provider.get_multiple(&ids).await?;
|
||||
|
||||
assert_eq!(res.len(), entries.len());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_get_versions() -> LauncherResult<()> {
|
||||
let cf = init!();
|
||||
|
||||
for (provider, entries) in DATA.iter() {
|
||||
if provider == &Provider::CurseForge && !cf {
|
||||
continue;
|
||||
}
|
||||
|
||||
let slugs = entries
|
||||
.iter()
|
||||
.map(|e| e.version_id.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let res = provider
|
||||
.get_versions(&slugs.iter().map(|s| s.to_string()).collect::<Vec<String>>())
|
||||
.await?;
|
||||
|
||||
assert_eq!(res.len(), slugs.len());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_download_packages() -> LauncherResult<()> {
|
||||
let cf = init!();
|
||||
|
||||
for (provider, entries) in DATA.iter() {
|
||||
if provider == &Provider::CurseForge && !cf {
|
||||
continue;
|
||||
}
|
||||
|
||||
for entry in entries {
|
||||
if entry.package_type == PackageType::ModPack {
|
||||
continue; // TODO: Skip modpacks for now
|
||||
}
|
||||
|
||||
let pkg = provider.get(entry.id).await?;
|
||||
let ver_id = entry.version_id.to_string();
|
||||
let versions = provider.get_versions(&[ver_id.clone()]).await?;
|
||||
let ver = versions.first().expect("No version found");
|
||||
|
||||
let db_model = packages::download_package(&pkg, ver, None).await?;
|
||||
assert!(db_model.hash == ver.files.first().unwrap().sha1);
|
||||
|
||||
let dir = Dirs::get_package_dir(
|
||||
&db_model.package_type,
|
||||
&db_model.provider,
|
||||
&db_model.package_id,
|
||||
)
|
||||
.await?;
|
||||
let dest = dir.join(packages::join_package_file(
|
||||
&dir,
|
||||
&db_model.version_id,
|
||||
&db_model.file_name,
|
||||
));
|
||||
|
||||
assert!(dest.exists(), "File does not exist: {dest:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: Add more tests
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
use onelauncher_entity::package::Provider;
|
||||
|
||||
use crate::api::packages::provider::ProviderExt;
|
||||
use crate::api::packages::{self};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::{Core, CoreOptions, Dirs};
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_get_provider() -> LauncherResult<()> {
|
||||
Core::initialize(CoreOptions::default()).await?;
|
||||
|
||||
let provider = Provider::Modrinth;
|
||||
|
||||
let res = provider.get("oneconfig").await?;
|
||||
|
||||
assert_eq!(res.id, "AibBIVmj");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_get_multiple() -> LauncherResult<()> {
|
||||
Core::initialize(CoreOptions::default()).await?;
|
||||
|
||||
let provider = Provider::Modrinth;
|
||||
|
||||
let res = provider
|
||||
.get_multiple(&["oneconfig".to_string(), "chatting".to_string()])
|
||||
.await?;
|
||||
|
||||
assert_eq!(res.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_get_versions() -> LauncherResult<()> {
|
||||
Core::initialize(CoreOptions::default()).await?;
|
||||
|
||||
let provider = Provider::Modrinth;
|
||||
|
||||
let res = provider
|
||||
.get_versions(&["ZvlCAdEF".to_string(), "GQANlg7p".to_string()])
|
||||
.await?;
|
||||
|
||||
assert_eq!(res.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_download_chatting() -> LauncherResult<()> {
|
||||
Core::initialize(CoreOptions::default()).await?;
|
||||
|
||||
let provider = Provider::Modrinth;
|
||||
|
||||
let pkg = provider.get("chatting").await?;
|
||||
let ver_id = pkg.version_ids.first().expect("No version found");
|
||||
let versions = provider.get_versions(&[ver_id.clone()]).await?;
|
||||
let ver = versions.first().expect("No version found");
|
||||
|
||||
let db_model = packages::download_package(&pkg, ver, None).await;
|
||||
assert!(db_model.is_ok_and(|m| m.display_name == pkg.name));
|
||||
|
||||
let dir = Dirs::get_package_dir(&pkg.package_type, &provider, &pkg.id).await?;
|
||||
let dest = dir.join(&ver.files.first().unwrap().file_name);
|
||||
|
||||
assert!(dest.exists(), "File does not exist: {dest:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: Add more tests
|
||||
|
|
@ -118,14 +118,14 @@ pub trait TauriLauncherApi {
|
|||
async fn get_package_versions(
|
||||
provider: Provider,
|
||||
slug: String,
|
||||
mc_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<GameLoader>>,
|
||||
mc_version: Option<String>,
|
||||
loader: Option<GameLoader>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> LauncherResult<Paginated<ManagedVersion>>;
|
||||
|
||||
#[taurpc(alias = "getPackageUser")]
|
||||
async fn get_package_user(provider: Provider, slug: String) -> LauncherResult<ManagedUser>;
|
||||
// #[taurpc(alias = "getPackageUser")]
|
||||
// async fn get_package_user(provider: Provider, slug: String) -> LauncherResult<ManagedUser>;
|
||||
|
||||
#[taurpc(alias = "downloadPackage")]
|
||||
async fn download_package(
|
||||
|
|
@ -485,24 +485,16 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
self,
|
||||
provider: Provider,
|
||||
slug: String,
|
||||
mc_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<GameLoader>>,
|
||||
mc_version: Option<String>,
|
||||
loader: Option<GameLoader>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> LauncherResult<Paginated<ManagedVersion>> {
|
||||
provider
|
||||
.get_versions_paginated(&slug, mc_versions, loaders, offset, limit)
|
||||
.get_versions_paginated(&slug, mc_version, loader, offset, limit)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_package_user(
|
||||
self,
|
||||
provider: Provider,
|
||||
slug: String,
|
||||
) -> LauncherResult<ManagedUser> {
|
||||
provider.get_user(&slug).await
|
||||
}
|
||||
|
||||
async fn download_package(
|
||||
self,
|
||||
provider: Provider,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ pub const MINECRAFT_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
|||
/// The Modrinth API base url.
|
||||
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com";
|
||||
/// The CurseForge API base url.
|
||||
pub const CURSEFORGE_API_URL: &str = "https://api.curseforge.com";
|
||||
pub const CURSEFORGE_API_URL: &str = "https://api.curseforge.com/v1";
|
||||
/// The Minecraft game ID on CurseForge.
|
||||
pub const CURSEFORGE_GAME_ID: u32 = 432;
|
||||
/// Our metadata API base url.
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ pub struct CoreOptions {
|
|||
}
|
||||
|
||||
impl Default for CoreOptions {
|
||||
#[cfg(not(test))]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
discord_client_id: None,
|
||||
|
|
@ -40,6 +41,26 @@ impl Default for CoreOptions {
|
|||
logger_filter: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
discord_client_id: std::env::var("DISCORD_CLIENT_ID").ok(),
|
||||
launcher_name: String::from("Launcher"),
|
||||
launcher_version: String::from(env!("CARGO_PKG_VERSION")),
|
||||
launcher_website: String::from("https://polyfrost.org/"),
|
||||
fetch_attempts: 3,
|
||||
msa_client_id: std::env::var("MSA_CLIENT_ID")
|
||||
.ok()
|
||||
.unwrap_or_else(|| String::from("00000000402b5328")),
|
||||
msa_redirect_uri: std::env::var("MSA_REDIRECT_URI")
|
||||
.ok()
|
||||
.unwrap_or_else(|| String::from("https://login.live.com/oauth20_desktop.srf")),
|
||||
curseforge_api_key: std::env::var("CURSEFORGE_API_KEY").ok(),
|
||||
logger_span_formatting: None,
|
||||
logger_filter: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Core {
|
||||
|
|
|
|||
|
|
@ -113,8 +113,8 @@ pub struct Ingress {
|
|||
pub last_sent: f64,
|
||||
}
|
||||
|
||||
// #[onelauncher_macro::specta]
|
||||
#[derive(Serialize, Deserialize, specta::Type, Debug, Clone)]
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum IngressType {
|
||||
Download { file_name: String },
|
||||
JavaPrepare,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ path = "src/lib.rs"
|
|||
default = []
|
||||
specta = [
|
||||
"dep:specta",
|
||||
"onelauncher_macro/specta",
|
||||
"interpulse/specta"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,8 @@ use sea_orm::DeriveValueType;
|
|||
use sea_orm::sea_query::Nullable;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// #[onelauncher_macro::specta]
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType, specta::Type,
|
||||
)]
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType)]
|
||||
pub struct Icon(String);
|
||||
|
||||
impl Nullable for Icon {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ use sea_orm::{DeriveActiveEnum, EnumIter};
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)]
|
||||
#[derive(
|
||||
Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[sea_orm(rs_type = "u8", db_type = "Integer")]
|
||||
pub enum PackageType {
|
||||
#[default]
|
||||
Mod = 0,
|
||||
ResourcePack = 1,
|
||||
Shader = 2,
|
||||
|
|
@ -29,6 +32,32 @@ impl PackageType {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<String> for PackageType {
|
||||
fn from(value: String) -> Self {
|
||||
match value.to_lowercase().as_str() {
|
||||
"mod" => Self::Mod,
|
||||
"resourcepack" => Self::ResourcePack,
|
||||
"shader" => Self::Shader,
|
||||
"datapack" => Self::DataPack,
|
||||
"modpack" => Self::ModPack,
|
||||
_ => Self::Mod, // default case
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for PackageType {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
&Self::Mod => "mod",
|
||||
&Self::ResourcePack => "resourcepack",
|
||||
&Self::Shader => "shader",
|
||||
&Self::DataPack => "datapack",
|
||||
&Self::ModPack => "modpack",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)]
|
||||
#[sea_orm(rs_type = "u8", db_type = "Integer")]
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ proc-macro = true
|
|||
|
||||
[features]
|
||||
default = []
|
||||
tauri = [ "specta" ]
|
||||
specta = []
|
||||
|
||||
[dependencies]
|
||||
syn = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -16,9 +16,16 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||
|
||||
for variant in &data_enum.variants {
|
||||
let v_ident = &variant.ident;
|
||||
let cfg_attrs = &variant
|
||||
.attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.path().is_ident("cfg"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match &variant.fields {
|
||||
Fields::Unit => {
|
||||
serialize_arms.push(quote! {
|
||||
#(#cfg_attrs)*
|
||||
Self::#v_ident => {
|
||||
let mut s = serializer.serialize_struct(stringify!(#ident), 2)?;
|
||||
s.serialize_field("type", stringify!(#v_ident))?;
|
||||
|
|
@ -43,6 +50,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||
|
||||
if has_from && !has_skip {
|
||||
serialize_arms.push(quote! {
|
||||
#(#cfg_attrs)*
|
||||
Self::#v_ident(inner) => {
|
||||
let mut s = serializer.serialize_struct(stringify!(#ident), 2)?;
|
||||
s.serialize_field("type", stringify!(#v_ident))?;
|
||||
|
|
@ -52,6 +60,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||
});
|
||||
} else {
|
||||
serialize_arms.push(quote! {
|
||||
#(#cfg_attrs)*
|
||||
Self::#v_ident(_) => {
|
||||
let mut s = serializer.serialize_struct(stringify!(#ident), 2)?;
|
||||
s.serialize_field("type", stringify!(#v_ident))?;
|
||||
|
|
@ -62,6 +71,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||
}
|
||||
} else {
|
||||
serialize_arms.push(quote! {
|
||||
#(#cfg_attrs)*
|
||||
Self::#v_ident(..) => {
|
||||
let mut s = serializer.serialize_struct(stringify!(#ident), 2)?;
|
||||
s.serialize_field("type", stringify!(#v_ident))?;
|
||||
|
|
@ -73,6 +83,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||
}
|
||||
Fields::Named(_) => {
|
||||
serialize_arms.push(quote! {
|
||||
#(#cfg_attrs)*
|
||||
Self::#v_ident { .. } => {
|
||||
let mut s = serializer.serialize_struct(stringify!(#ident), 2)?;
|
||||
s.serialize_field("type", stringify!(#v_ident))?;
|
||||
|
|
@ -99,7 +110,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||
};
|
||||
|
||||
let output = quote! {
|
||||
#[cfg_attr(feature = "specta", derive(onelauncher_macro::SerializedError))]
|
||||
#[derive(onelauncher_macro::SerializedError)]
|
||||
#input
|
||||
|
||||
#serialize_impl
|
||||
|
|
@ -160,6 +171,7 @@ pub fn error_derive(input: TokenStream) -> TokenStream {
|
|||
|
||||
let enum_name_str = enum_name.to_string();
|
||||
let output = quote! {
|
||||
#[cfg(feature = "specta")]
|
||||
#[derive(Debug, specta::Type, serde::Serialize)]
|
||||
#[specta(remote = #enum_name)]
|
||||
#[specta(tag = "type", content = "data")]
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
#[cfg(feature = "specta")]
|
||||
mod specta;
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
mod error;
|
||||
|
||||
mod pin;
|
||||
mod specta;
|
||||
|
||||
/// This macro is used to derive the `specta::Type` trait for the annotated item.
|
||||
/// It's mainly meant for entity-generation with sea-orm-cli (it doesn't like commas in attributes)
|
||||
|
|
@ -23,7 +19,6 @@ mod pin;
|
|||
/// #[cfg_attr(feature = "specta", specta(rename = #rename))]
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
#[cfg(feature = "specta")]
|
||||
pub fn specta(
|
||||
attr: proc_macro::TokenStream,
|
||||
item: proc_macro::TokenStream,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue