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, modrinth: Arc, } impl MultiplatformPlatform { pub fn new( curseforge: Arc, modrinth: Arc, ) -> Self { Self { curseforge, modrinth, } } /// Try to fetch a project, returning Ok(None) for "not found" errors. async fn try_request_project( &self, client: &Arc, identifier: &str, ) -> Result> { 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 { // 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> { // 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 { // 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> { // 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> { 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> { 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) } }