platform: add CurseForge side detection from categories
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I62c5117ed97bbc2389330720b4761a716a6a6964
This commit is contained in:
parent
5385c0f4ed
commit
27160a1eda
2 changed files with 195 additions and 8 deletions
|
|
@ -8,13 +8,12 @@ use std::sync::Arc;
|
||||||
pub use curseforge::CurseForgePlatform;
|
pub use curseforge::CurseForgePlatform;
|
||||||
pub use github::GitHubPlatform;
|
pub use github::GitHubPlatform;
|
||||||
pub use modrinth::ModrinthPlatform;
|
pub use modrinth::ModrinthPlatform;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
pub use traits::PlatformClient;
|
pub use traits::PlatformClient;
|
||||||
|
|
||||||
use crate::{error::Result, rate_limiter::RateLimiter};
|
use crate::{error::Result, rate_limiter::RateLimiter};
|
||||||
|
|
||||||
static RATE_LIMITER: Lazy<Arc<RateLimiter>> =
|
static RATE_LIMITER: std::sync::LazyLock<Arc<RateLimiter>> =
|
||||||
Lazy::new(|| Arc::new(RateLimiter::new(None)));
|
std::sync::LazyLock::new(|| Arc::new(RateLimiter::new(None)));
|
||||||
|
|
||||||
pub fn create_platform(
|
pub fn create_platform(
|
||||||
platform: &str,
|
platform: &str,
|
||||||
|
|
|
||||||
|
|
@ -66,11 +66,81 @@ impl CurseForgePlatform {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine project side based on `CurseForge` categories.
|
||||||
|
/// `CurseForge` doesn't have explicit client/server fields like Modrinth,
|
||||||
|
/// so we infer from category names and IDs.
|
||||||
|
fn detect_side_from_categories(
|
||||||
|
categories: &[CurseForgeCategory],
|
||||||
|
) -> ProjectSide {
|
||||||
|
// Known client-only category indicators (slugs and partial name matches)
|
||||||
|
const CLIENT_INDICATORS: &[&str] = &[
|
||||||
|
"client",
|
||||||
|
"hud",
|
||||||
|
"gui",
|
||||||
|
"cosmetic",
|
||||||
|
"shader",
|
||||||
|
"optifine",
|
||||||
|
"resource-pack",
|
||||||
|
"texture",
|
||||||
|
"minimap",
|
||||||
|
"tooltip",
|
||||||
|
"inventory",
|
||||||
|
"quality-of-life", // Often client-side QoL
|
||||||
|
];
|
||||||
|
|
||||||
|
// Known server-only category indicators
|
||||||
|
const SERVER_INDICATORS: &[&str] = &[
|
||||||
|
"server-utility",
|
||||||
|
"bukkit",
|
||||||
|
"spigot",
|
||||||
|
"paper",
|
||||||
|
"admin-tools",
|
||||||
|
"anti-grief",
|
||||||
|
"economy",
|
||||||
|
"permissions",
|
||||||
|
"chat",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut client_score = 0;
|
||||||
|
let mut server_score = 0;
|
||||||
|
|
||||||
|
for category in categories {
|
||||||
|
let slug_lower = category.slug.to_lowercase();
|
||||||
|
let name_lower = category.name.to_lowercase();
|
||||||
|
|
||||||
|
for indicator in CLIENT_INDICATORS {
|
||||||
|
if slug_lower.contains(indicator) || name_lower.contains(indicator) {
|
||||||
|
client_score += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for indicator in SERVER_INDICATORS {
|
||||||
|
if slug_lower.contains(indicator) || name_lower.contains(indicator) {
|
||||||
|
server_score += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only assign a specific side if there's clear indication
|
||||||
|
// and not conflicting signals
|
||||||
|
if client_score > 0 && server_score == 0 {
|
||||||
|
ProjectSide::Client
|
||||||
|
} else if server_score > 0 && client_score == 0 {
|
||||||
|
ProjectSide::Server
|
||||||
|
} else {
|
||||||
|
// Default to Both - works on both client and server
|
||||||
|
ProjectSide::Both
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn convert_project(&self, cf_project: CurseForgeProject) -> Project {
|
fn convert_project(&self, cf_project: CurseForgeProject) -> Project {
|
||||||
let pakku_id = generate_pakku_id();
|
let pakku_id = generate_pakku_id();
|
||||||
let project_type = Self::map_class_id(cf_project.class_id.unwrap_or(6));
|
let project_type = Self::map_class_id(cf_project.class_id.unwrap_or(6));
|
||||||
|
|
||||||
let mut project = Project::new(pakku_id, project_type, ProjectSide::Both);
|
// Detect side from categories
|
||||||
|
let side = Self::detect_side_from_categories(&cf_project.categories);
|
||||||
|
|
||||||
|
let mut project = Project::new(pakku_id, project_type, side);
|
||||||
|
|
||||||
project.add_platform(
|
project.add_platform(
|
||||||
"curseforge".to_string(),
|
"curseforge".to_string(),
|
||||||
|
|
@ -317,11 +387,20 @@ impl PlatformClient for CurseForgePlatform {
|
||||||
// CurseForge API models
|
// CurseForge API models
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
struct CurseForgeProject {
|
struct CurseForgeProject {
|
||||||
id: u32,
|
id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
slug: String,
|
slug: String,
|
||||||
#[serde(rename = "classId")]
|
#[serde(rename = "classId")]
|
||||||
class_id: Option<u32>,
|
class_id: Option<u32>,
|
||||||
|
#[serde(default)]
|
||||||
|
categories: Vec<CurseForgeCategory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
struct CurseForgeCategory {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
slug: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
|
@ -381,3 +460,112 @@ struct CurseForgeFilesResponse {
|
||||||
struct CurseForgeSearchResponse {
|
struct CurseForgeSearchResponse {
|
||||||
data: Vec<CurseForgeProject>,
|
data: Vec<CurseForgeProject>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn make_category(id: u32, name: &str, slug: &str) -> CurseForgeCategory {
|
||||||
|
CurseForgeCategory {
|
||||||
|
id,
|
||||||
|
name: name.to_string(),
|
||||||
|
slug: slug.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_client_only() {
|
||||||
|
// HUD mod should be client-only
|
||||||
|
let categories = vec![
|
||||||
|
make_category(1, "HUD Mods", "hud"),
|
||||||
|
make_category(2, "Fabric", "fabric"),
|
||||||
|
];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Client);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_server_only() {
|
||||||
|
// Server utility should be server-only
|
||||||
|
let categories = vec![
|
||||||
|
make_category(1, "Server Utility", "server-utility"),
|
||||||
|
make_category(2, "Bukkit Plugins", "bukkit"),
|
||||||
|
];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Server);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_both() {
|
||||||
|
// Generic mod categories should be both
|
||||||
|
let categories = vec![
|
||||||
|
make_category(1, "Technology", "technology"),
|
||||||
|
make_category(2, "Fabric", "fabric"),
|
||||||
|
];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Both);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_conflicting_signals() {
|
||||||
|
// Mixed categories should default to both
|
||||||
|
let categories = vec![
|
||||||
|
make_category(1, "Client HUD", "client-hud"),
|
||||||
|
make_category(2, "Server Utility", "server-utility"),
|
||||||
|
];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Both);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_empty_categories() {
|
||||||
|
let categories = vec![];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Both);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_gui_client() {
|
||||||
|
let categories =
|
||||||
|
vec![make_category(1, "GUI Enhancement", "gui-enhancement")];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Client);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_side_permissions_server() {
|
||||||
|
let categories = vec![make_category(1, "Permissions", "permissions")];
|
||||||
|
let side = CurseForgePlatform::detect_side_from_categories(&categories);
|
||||||
|
assert_eq!(side, ProjectSide::Server);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_map_class_id() {
|
||||||
|
assert_eq!(CurseForgePlatform::map_class_id(6), ProjectType::Mod);
|
||||||
|
assert_eq!(
|
||||||
|
CurseForgePlatform::map_class_id(12),
|
||||||
|
ProjectType::ResourcePack
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
CurseForgePlatform::map_class_id(6945),
|
||||||
|
ProjectType::DataPack
|
||||||
|
);
|
||||||
|
assert_eq!(CurseForgePlatform::map_class_id(6552), ProjectType::Shader);
|
||||||
|
assert_eq!(CurseForgePlatform::map_class_id(17), ProjectType::World);
|
||||||
|
assert_eq!(CurseForgePlatform::map_class_id(9999), ProjectType::Mod); // Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_map_release_type() {
|
||||||
|
assert_eq!(
|
||||||
|
CurseForgePlatform::map_release_type(1),
|
||||||
|
ReleaseType::Release
|
||||||
|
);
|
||||||
|
assert_eq!(CurseForgePlatform::map_release_type(2), ReleaseType::Beta);
|
||||||
|
assert_eq!(CurseForgePlatform::map_release_type(3), ReleaseType::Alpha);
|
||||||
|
assert_eq!(
|
||||||
|
CurseForgePlatform::map_release_type(99),
|
||||||
|
ReleaseType::Release
|
||||||
|
); // Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue