Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ic4d2bd6f3baf97ce30dbf8709331f6f66a6a6964
234 lines
6.8 KiB
Rust
234 lines
6.8 KiB
Rust
use std::sync::Arc;
|
|
|
|
use async_trait::async_trait;
|
|
|
|
use super::traits::PlatformClient;
|
|
use crate::{
|
|
error::{PakkerError, Result},
|
|
model::{Project, ProjectFile},
|
|
};
|
|
|
|
/// Multiplatform platform client that aggregates CurseForge and Modrinth.
|
|
/// It attempts to resolve projects on both platforms and cross-references
|
|
/// them via slugs when a project exists on only one platform.
|
|
pub struct MultiplatformPlatform {
|
|
curseforge: Arc<dyn PlatformClient>,
|
|
modrinth: Arc<dyn PlatformClient>,
|
|
}
|
|
|
|
impl MultiplatformPlatform {
|
|
pub fn new(
|
|
curseforge: Arc<dyn PlatformClient>,
|
|
modrinth: Arc<dyn PlatformClient>,
|
|
) -> Self {
|
|
Self {
|
|
curseforge,
|
|
modrinth,
|
|
}
|
|
}
|
|
|
|
/// Try to fetch a project, returning Ok(None) for "not found" errors.
|
|
async fn try_request_project(
|
|
&self,
|
|
client: &Arc<dyn PlatformClient>,
|
|
identifier: &str,
|
|
) -> Result<Option<Project>> {
|
|
match client.request_project(identifier, &[], &[]).await {
|
|
Ok(project) => Ok(Some(project)),
|
|
Err(e) => {
|
|
let is_not_found = matches!(
|
|
e,
|
|
PakkerError::ProjectNotFound(_) | PakkerError::InvalidResponse(_)
|
|
);
|
|
if is_not_found { Ok(None) } else { Err(e) }
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl PlatformClient for MultiplatformPlatform {
|
|
async fn request_project(
|
|
&self,
|
|
identifier: &str,
|
|
_mc_versions: &[String],
|
|
_loaders: &[String],
|
|
) -> Result<Project> {
|
|
// Try both platforms in parallel
|
|
let cf_future = self.try_request_project(&self.curseforge, identifier);
|
|
let mr_future = self.try_request_project(&self.modrinth, identifier);
|
|
|
|
let (cf_result, mr_result) = tokio::join!(cf_future, mr_future);
|
|
|
|
// Handle results - extract Options, propagate first error if both fail
|
|
let (cf_project, mr_project) = match (cf_result, mr_result) {
|
|
(Ok(Some(cf)), Ok(Some(mr))) => (Some(cf), Some(mr)),
|
|
(Ok(None), Ok(None)) => (None, None),
|
|
(Ok(None), Ok(Some(mr))) => (None, Some(mr)),
|
|
(Ok(Some(cf)), Ok(None)) => (Some(cf), None),
|
|
(Err(_e), Ok(None)) | (Ok(None), Err(_e)) => (None, None),
|
|
(Err(e), Ok(Some(_))) | (Ok(Some(_)), Err(e)) => {
|
|
return Err(e);
|
|
},
|
|
(Err(e), Err(_)) => return Err(e),
|
|
};
|
|
|
|
// Cross-reference: if project exists on only one platform, fetch from the
|
|
// other using the slug
|
|
let mut cf_project = cf_project;
|
|
let mut mr_project = mr_project;
|
|
|
|
let mr_found_and_cf_missing = mr_project.is_some() && cf_project.is_none();
|
|
if mr_found_and_cf_missing
|
|
&& let Some(ref mr) = mr_project
|
|
&& let Some(cf_slug) = mr.slug.get("curseforge")
|
|
&& let Ok(Some(cf)) =
|
|
self.curseforge.request_project_from_slug(cf_slug).await
|
|
{
|
|
cf_project = Some(cf);
|
|
}
|
|
let cf_found_and_mr_missing = cf_project.is_some() && mr_project.is_none();
|
|
if cf_found_and_mr_missing
|
|
&& let Some(ref cf) = cf_project
|
|
&& let Some(mr_slug) = cf.slug.get("modrinth")
|
|
&& let Ok(Some(mr)) =
|
|
self.modrinth.request_project_from_slug(mr_slug).await
|
|
{
|
|
mr_project = Some(mr);
|
|
}
|
|
|
|
// Merge projects or return whichever was found
|
|
let combined = match (cf_project, mr_project) {
|
|
(Some(cf), Some(mr)) => cf.merged(mr)?,
|
|
(Some(cf), None) => cf,
|
|
(None, Some(mr)) => mr,
|
|
(None, None) => {
|
|
return Err(PakkerError::ProjectNotFound(identifier.to_string()));
|
|
},
|
|
};
|
|
|
|
Ok(combined)
|
|
}
|
|
|
|
async fn request_project_files(
|
|
&self,
|
|
project_id: &str,
|
|
mc_versions: &[String],
|
|
loaders: &[String],
|
|
) -> Result<Vec<ProjectFile>> {
|
|
// Multiplatform doesn't directly support files - use
|
|
// request_project_with_files
|
|
let project = self
|
|
.request_project_with_files(project_id, mc_versions, loaders)
|
|
.await?;
|
|
Ok(project.files)
|
|
}
|
|
|
|
async fn request_project_with_files(
|
|
&self,
|
|
identifier: &str,
|
|
mc_versions: &[String],
|
|
loaders: &[String],
|
|
) -> Result<Project> {
|
|
// First get the combined project from both platforms
|
|
let mut project = self
|
|
.request_project(identifier, mc_versions, loaders)
|
|
.await?;
|
|
|
|
// Now fetch files from both platforms in parallel
|
|
let cf_project_id = project.id.get("curseforge").cloned();
|
|
let mr_project_id = project.id.get("modrinth").cloned();
|
|
|
|
let cf_files_future = async {
|
|
if let Some(ref id) = cf_project_id {
|
|
self
|
|
.curseforge
|
|
.request_project_files(id, mc_versions, loaders)
|
|
.await
|
|
} else {
|
|
Ok(Vec::new())
|
|
}
|
|
};
|
|
|
|
let mr_files_future = async {
|
|
if let Some(ref id) = mr_project_id {
|
|
self
|
|
.modrinth
|
|
.request_project_files(id, mc_versions, loaders)
|
|
.await
|
|
} else {
|
|
Ok(Vec::new())
|
|
}
|
|
};
|
|
|
|
let (cf_files, mr_files) = tokio::join!(cf_files_future, mr_files_future);
|
|
|
|
let mut all_files = cf_files?;
|
|
all_files.extend(mr_files?);
|
|
|
|
project.files = all_files;
|
|
Ok(project)
|
|
}
|
|
|
|
async fn lookup_by_hash(&self, hash: &str) -> Result<Option<Project>> {
|
|
// Try both platforms in parallel
|
|
let cf_future = self.curseforge.lookup_by_hash(hash);
|
|
let mr_future = self.modrinth.lookup_by_hash(hash);
|
|
|
|
let (cf_result, mr_result) = tokio::join!(cf_future, mr_future);
|
|
|
|
match (cf_result?, mr_result?) {
|
|
(Some(cf), Some(mr)) => cf.merged(mr).map(Some),
|
|
(Some(project), None) | (None, Some(project)) => Ok(Some(project)),
|
|
(None, None) => Ok(None),
|
|
}
|
|
}
|
|
|
|
async fn request_project_from_slug(
|
|
&self,
|
|
slug: &str,
|
|
) -> Result<Option<Project>> {
|
|
let cf_future = self.curseforge.request_project_from_slug(slug);
|
|
let mr_future = self.modrinth.request_project_from_slug(slug);
|
|
|
|
let (cf_result, mr_result) = tokio::join!(cf_future, mr_future);
|
|
|
|
match (cf_result, mr_result) {
|
|
(Ok(Some(cf)), Ok(Some(mr))) => cf.merged(mr).map(Some),
|
|
(Ok(Some(project)), Ok(None)) | (Ok(None), Ok(Some(project))) => {
|
|
Ok(Some(project))
|
|
},
|
|
(Ok(None), Ok(None)) => Ok(None),
|
|
(Err(e), _) | (_, Err(e)) => Err(e),
|
|
}
|
|
}
|
|
|
|
/// Delegates to both CurseForge and Modrinth in parallel, then deduplicates
|
|
/// results.
|
|
async fn request_projects_from_hashes(
|
|
&self,
|
|
hashes: &[String],
|
|
algorithm: &str,
|
|
) -> Result<Vec<Project>> {
|
|
let cf_future = self
|
|
.curseforge
|
|
.request_projects_from_hashes(hashes, algorithm);
|
|
let mr_future = self
|
|
.modrinth
|
|
.request_projects_from_hashes(hashes, algorithm);
|
|
|
|
let (cf_projects, mr_projects) = tokio::join!(cf_future, mr_future);
|
|
|
|
let mut all_projects = cf_projects?;
|
|
for mr_project in mr_projects? {
|
|
if !all_projects.iter().any(|p| {
|
|
p.id.get("modrinth") == mr_project.id.get("modrinth")
|
|
|| p.id.get("curseforge") == mr_project.id.get("curseforge")
|
|
}) {
|
|
all_projects.push(mr_project);
|
|
}
|
|
}
|
|
|
|
Ok(all_projects)
|
|
}
|
|
}
|