initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ife1391ed23a1e7f388b1b5eca90b9ea76a6a6964
This commit is contained in:
commit
ef28bdaeb4
63 changed files with 17292 additions and 0 deletions
282
src/platform/modrinth.rs
Normal file
282
src/platform/modrinth.rs
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::traits::PlatformClient;
|
||||
use crate::{
|
||||
error::{PakkerError, Result},
|
||||
model::{Project, ProjectFile, ProjectSide, ProjectType, ReleaseType},
|
||||
utils::generate_pakku_id,
|
||||
};
|
||||
|
||||
const MODRINTH_API_BASE: &str = "https://api.modrinth.com/v2";
|
||||
|
||||
pub struct ModrinthPlatform {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl ModrinthPlatform {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_project_type(type_str: &str) -> ProjectType {
|
||||
match type_str {
|
||||
"mod" => ProjectType::Mod,
|
||||
"resourcepack" => ProjectType::ResourcePack,
|
||||
"datapack" => ProjectType::DataPack,
|
||||
"shader" => ProjectType::Shader,
|
||||
_ => ProjectType::Mod,
|
||||
}
|
||||
}
|
||||
|
||||
const fn map_side(client: bool, server: bool) -> ProjectSide {
|
||||
match (client, server) {
|
||||
(true, true) => ProjectSide::Both,
|
||||
(true, false) => ProjectSide::Client,
|
||||
(false, true) => ProjectSide::Server,
|
||||
_ => ProjectSide::Both,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_release_type(type_str: &str) -> ReleaseType {
|
||||
match type_str {
|
||||
"release" => ReleaseType::Release,
|
||||
"beta" => ReleaseType::Beta,
|
||||
"alpha" => ReleaseType::Alpha,
|
||||
_ => ReleaseType::Release,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_project(&self, mr_project: ModrinthProject) -> Project {
|
||||
let pakku_id = generate_pakku_id();
|
||||
let mut project = Project::new(
|
||||
pakku_id,
|
||||
Self::map_project_type(&mr_project.project_type),
|
||||
Self::map_side(
|
||||
mr_project.client_side != "unsupported",
|
||||
mr_project.server_side != "unsupported",
|
||||
),
|
||||
);
|
||||
|
||||
project.add_platform(
|
||||
"modrinth".to_string(),
|
||||
mr_project.id.clone(),
|
||||
mr_project.slug.clone(),
|
||||
mr_project.title,
|
||||
);
|
||||
|
||||
project
|
||||
}
|
||||
|
||||
fn convert_version(
|
||||
&self,
|
||||
mr_version: ModrinthVersion,
|
||||
project_id: &str,
|
||||
) -> ProjectFile {
|
||||
let mut hashes = HashMap::new();
|
||||
|
||||
// Get primary file
|
||||
let primary_file = mr_version
|
||||
.files
|
||||
.iter()
|
||||
.find(|f| f.primary)
|
||||
.or_else(|| mr_version.files.first())
|
||||
.expect("Version must have at least one file");
|
||||
|
||||
for (algo, hash) in &primary_file.hashes {
|
||||
hashes.insert(algo.clone(), hash.clone());
|
||||
}
|
||||
|
||||
ProjectFile {
|
||||
file_type: "mod".to_string(),
|
||||
file_name: primary_file.filename.clone(),
|
||||
mc_versions: mr_version.game_versions.clone(),
|
||||
loaders: mr_version.loaders.clone(),
|
||||
release_type: Self::map_release_type(&mr_version.version_type),
|
||||
url: primary_file.url.clone(),
|
||||
id: mr_version.id.clone(),
|
||||
parent_id: project_id.to_string(),
|
||||
hashes,
|
||||
required_dependencies: mr_version
|
||||
.dependencies
|
||||
.iter()
|
||||
.filter(|d| d.dependency_type == "required")
|
||||
.filter_map(|d| d.project_id.clone())
|
||||
.collect(),
|
||||
size: primary_file.size,
|
||||
date_published: mr_version.date_published.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PlatformClient for ModrinthPlatform {
|
||||
async fn request_project(
|
||||
&self,
|
||||
identifier: &str,
|
||||
_mc_versions: &[String],
|
||||
_loaders: &[String],
|
||||
) -> Result<Project> {
|
||||
let url = format!("{MODRINTH_API_BASE}/project/{identifier}");
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PakkerError::ProjectNotFound(identifier.to_string()));
|
||||
}
|
||||
|
||||
let mr_project: ModrinthProject = response.json().await?;
|
||||
Ok(self.convert_project(mr_project))
|
||||
}
|
||||
|
||||
async fn request_project_files(
|
||||
&self,
|
||||
project_id: &str,
|
||||
mc_versions: &[String],
|
||||
loaders: &[String],
|
||||
) -> Result<Vec<ProjectFile>> {
|
||||
let mut url = format!("{MODRINTH_API_BASE}/project/{project_id}/version");
|
||||
|
||||
// Add query parameters
|
||||
let mut params = vec![];
|
||||
if !mc_versions.is_empty() {
|
||||
params.push(format!(
|
||||
"game_versions=[{}]",
|
||||
mc_versions
|
||||
.iter()
|
||||
.map(|v| format!("\"{v}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
));
|
||||
}
|
||||
if !loaders.is_empty() {
|
||||
params.push(format!(
|
||||
"loaders=[{}]",
|
||||
loaders
|
||||
.iter()
|
||||
.map(|l| format!("\"{l}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
));
|
||||
}
|
||||
|
||||
if !params.is_empty() {
|
||||
url.push('?');
|
||||
url.push_str(¶ms.join("&"));
|
||||
}
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PakkerError::ProjectNotFound(project_id.to_string()));
|
||||
}
|
||||
|
||||
let mr_versions: Vec<ModrinthVersion> = response.json().await?;
|
||||
|
||||
Ok(
|
||||
mr_versions
|
||||
.into_iter()
|
||||
.map(|v| self.convert_version(v, project_id))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn request_project_with_files(
|
||||
&self,
|
||||
identifier: &str,
|
||||
mc_versions: &[String],
|
||||
loaders: &[String],
|
||||
) -> Result<Project> {
|
||||
let mut project = self
|
||||
.request_project(identifier, mc_versions, loaders)
|
||||
.await?;
|
||||
let project_id = project
|
||||
.get_platform_id("modrinth")
|
||||
.ok_or_else(|| {
|
||||
PakkerError::InternalError("Missing modrinth ID".to_string())
|
||||
})?
|
||||
.clone();
|
||||
|
||||
let files = self
|
||||
.request_project_files(&project_id, mc_versions, loaders)
|
||||
.await?;
|
||||
project.files = files;
|
||||
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
async fn lookup_by_hash(&self, hash: &str) -> Result<Option<Project>> {
|
||||
// Modrinth uses SHA-1 hash for file lookups
|
||||
let url = format!("{MODRINTH_API_BASE}/version_file/{hash}");
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
|
||||
if response.status().as_u16() == 404 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PakkerError::PlatformApiError(format!(
|
||||
"Modrinth API error: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
let version_data: serde_json::Value = response.json().await?;
|
||||
|
||||
let project_id = version_data["project_id"].as_str().ok_or_else(|| {
|
||||
PakkerError::InvalidResponse("Missing project_id".to_string())
|
||||
})?;
|
||||
|
||||
self
|
||||
.request_project_with_files(project_id, &[], &[])
|
||||
.await
|
||||
.map(Some)
|
||||
}
|
||||
}
|
||||
|
||||
// Modrinth API models
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct ModrinthProject {
|
||||
id: String,
|
||||
slug: String,
|
||||
title: String,
|
||||
#[serde(rename = "project_type")]
|
||||
project_type: String,
|
||||
client_side: String,
|
||||
server_side: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct ModrinthVersion {
|
||||
id: String,
|
||||
project_id: String,
|
||||
name: String,
|
||||
version_number: String,
|
||||
game_versions: Vec<String>,
|
||||
version_type: String,
|
||||
loaders: Vec<String>,
|
||||
date_published: String,
|
||||
files: Vec<ModrinthFile>,
|
||||
dependencies: Vec<ModrinthDependency>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct ModrinthFile {
|
||||
hashes: HashMap<String, String>,
|
||||
url: String,
|
||||
filename: String,
|
||||
primary: bool,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct ModrinthDependency {
|
||||
project_id: Option<String>,
|
||||
dependency_type: String,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue