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 github::GitHubPlatform;
|
||||
pub use modrinth::ModrinthPlatform;
|
||||
use once_cell::sync::Lazy;
|
||||
pub use traits::PlatformClient;
|
||||
|
||||
use crate::{error::Result, rate_limiter::RateLimiter};
|
||||
|
||||
static RATE_LIMITER: Lazy<Arc<RateLimiter>> =
|
||||
Lazy::new(|| Arc::new(RateLimiter::new(None)));
|
||||
static RATE_LIMITER: std::sync::LazyLock<Arc<RateLimiter>> =
|
||||
std::sync::LazyLock::new(|| Arc::new(RateLimiter::new(None)));
|
||||
|
||||
pub fn create_platform(
|
||||
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 {
|
||||
let pakku_id = generate_pakku_id();
|
||||
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(
|
||||
"curseforge".to_string(),
|
||||
|
|
@ -317,11 +387,20 @@ impl PlatformClient for CurseForgePlatform {
|
|||
// CurseForge API models
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct CurseForgeProject {
|
||||
id: u32,
|
||||
name: String,
|
||||
slug: String,
|
||||
id: u32,
|
||||
name: String,
|
||||
slug: String,
|
||||
#[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)]
|
||||
|
|
@ -381,3 +460,112 @@ struct CurseForgeFilesResponse {
|
|||
struct CurseForgeSearchResponse {
|
||||
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