From 3fa8e7f150c2d6bf1626bd6d85f161e40ef13009 Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Wed, 16 Jul 2025 21:27:01 +0200 Subject: [PATCH] feat: modrinth search implementation --- packages/core/examples/packages.rs | 11 +- packages/core/src/api/packages/data.rs | 38 ++++- .../core/src/api/packages/provider/mod.rs | 6 +- .../src/api/packages/provider/modrinth.rs | 154 +++++++++++++++++- packages/core/src/api/tauri/commands.rs | 9 +- packages/core/src/utils/pagination.rs | 6 +- 6 files changed, 203 insertions(+), 21 deletions(-) diff --git a/packages/core/examples/packages.rs b/packages/core/examples/packages.rs index 68b8eaf3..64bc84ce 100644 --- a/packages/core/examples/packages.rs +++ b/packages/core/examples/packages.rs @@ -1,4 +1,4 @@ -use onelauncher_core::{api::{cluster, packages::{self, download_package, provider::ProviderExt}, proxy::ProxyDynamic}, error::LauncherResult, initialize_core, store::{CoreOptions, Dirs}}; +use onelauncher_core::{api::{cluster, packages::{self, data::{SearchQuery, Sort}, download_package, provider::ProviderExt}, proxy::ProxyDynamic}, error::LauncherResult, initialize_core, store::{CoreOptions, Dirs}}; use onelauncher_entity::{loader::GameLoader, package::Provider}; #[tokio::main] @@ -19,6 +19,15 @@ pub async fn main() -> LauncherResult<()> { // Fetch our package and its versions let provider = Provider::Modrinth; + + println!("search results: {:#?}", provider.search(&SearchQuery { + query: Some("chatting".to_string()), + offset: Some(0), + limit: Some(10), + sort: Some(Sort::Newest), + filters: None, + }).await?); + let package = &provider.get("chatting").await?; let versions = provider.get_versions(package.version_ids.as_slice()).await?; diff --git a/packages/core/src/api/packages/data.rs b/packages/core/src/api/packages/data.rs index c50c5cb0..d046ea63 100644 --- a/packages/core/src/api/packages/data.rs +++ b/packages/core/src/api/packages/data.rs @@ -26,16 +26,52 @@ pub enum Sort { Updated } +impl std::fmt::Display for Sort { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + Self::Relevance => "relevance", + Self::Downloads => "downloads", + Self::Newest => "newest", + Self::Updated => "updated", + }) + } +} + #[onelauncher_macro::specta] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SearchQuery { pub query: Option, pub offset: Option, pub limit: Option, - pub sort: Option, + pub sort: Option, pub filters: Option, } +#[onelauncher_macro::specta] +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SearchResult { + pub project_id: String, + pub project_type: String, + pub slug: String, + pub author: String, + pub title: String, + pub description: String, + pub categories: Vec, + pub display_categories: Vec, + pub versions: Vec, + pub downloads: usize, + pub icon_url: String, + pub date_created: DateTime, + pub date_modified: DateTime, + pub latest_version: String, + pub license: String, + pub client_side: PackageSide, + pub server_side: PackageSide, + /// List of URLs to images + pub gallery: Vec, + // pub color: i64, +} + #[onelauncher_macro::specta] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/packages/core/src/api/packages/provider/mod.rs b/packages/core/src/api/packages/provider/mod.rs index b7e8646a..375fea85 100644 --- a/packages/core/src/api/packages/provider/mod.rs +++ b/packages/core/src/api/packages/provider/mod.rs @@ -1,7 +1,7 @@ use onelauncher_entity::loader::GameLoader; use onelauncher_entity::package::Provider; -use crate::error::LauncherResult; +use crate::{api::packages::data::SearchResult, error::LauncherResult}; use crate::utils::pagination::Paginated; use super::data::{ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery}; @@ -12,7 +12,7 @@ pub use modrinth::ModrinthProviderImpl; #[async_trait::async_trait] pub trait ProviderExt { - async fn search(&self, query: &SearchQuery) -> LauncherResult>; + async fn search(&self, query: &SearchQuery) -> LauncherResult>; async fn get(&self, slug: &str) -> LauncherResult; async fn get_multiple(&self, slugs: &[String]) -> LauncherResult>; async fn get_versions_by_hashes( @@ -44,7 +44,7 @@ pub trait ProviderExt { #[async_trait::async_trait] impl ProviderExt for Provider { - async fn search(&self, query: &SearchQuery) -> LauncherResult> { + async fn search(&self, query: &SearchQuery) -> LauncherResult> { match self { Self::Modrinth => ModrinthProviderImpl.search(query).await, _ => todo!("unimplemented provider"), diff --git a/packages/core/src/api/packages/provider/modrinth.rs b/packages/core/src/api/packages/provider/modrinth.rs index 0a5946a2..2712b28c 100644 --- a/packages/core/src/api/packages/provider/modrinth.rs +++ b/packages/core/src/api/packages/provider/modrinth.rs @@ -1,8 +1,6 @@ use super::ProviderExt; use crate::api::packages::data::{ - ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency, ManagedVersionFile, - PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageLicense, PackageLinks, - PackageReleaseType, PackageSide, PackageStatus, SearchQuery, DEFAULT_LIMIT, + ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency, ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageLicense, PackageLinks, PackageReleaseType, PackageSide, PackageStatus, SearchQuery, SearchResult, DEFAULT_LIMIT }; use crate::error::LauncherResult; use crate::utils::http; @@ -39,14 +37,69 @@ impl ModrinthProviderImpl { #[async_trait::async_trait] impl ProviderExt for ModrinthProviderImpl { - async fn search(&self, query: &SearchQuery) -> LauncherResult> { + async fn search(&self, query: &SearchQuery) -> LauncherResult> { let mut url = Url::parse(url!("/search"))?; - let mut params = url.query_pairs_mut(); - params.append_pair("limit", &query.limit.unwrap_or(DEFAULT_LIMIT).to_string()); - params.append_pair("offset", &query.offset.unwrap_or(0).to_string()); + { + let mut params = url.query_pairs_mut(); - todo!() + params.append_pair("limit", &query.limit.unwrap_or(DEFAULT_LIMIT).to_string()); + params.append_pair("offset", &query.offset.unwrap_or(0).to_string()); + params.append_pair("query", query.query.clone().unwrap_or_default().as_ref()); + + if let Some(filters) = &query.filters { + let mut builder = FacetBuilder::builder(); + + if let Some(categories) = &filters.categories { + for category in categories { + builder.and(Facet("categories".to_string(), FacetOperation::Eq, category.to_string())); + } + } + + if let Some(mc_versions) = &filters.game_versions { + for mc_version in mc_versions { + builder.and(Facet("versions".to_string(), FacetOperation::Eq, mc_version.to_string())); + } + } + + if let Some(package_types) = &filters.package_types { + for package_type in package_types { + builder.and(Facet("project_type".to_string(), FacetOperation::Eq, package_type_to_string(package_type))); + } + } + + if let Some(loaders) = &filters.loaders { + for loader in loaders { + builder.and(Facet("loaders".to_string(), FacetOperation::Eq, loader.to_string())); + } + } + + params.append_pair("facets", &builder.build()); + } + + if let Some(sort) = &query.sort { + params.append_pair("index", &sort.to_string()); + } + } + + let url = url.as_str(); + + #[derive(Deserialize)] + struct SearchResults { + pub hits: Vec, + pub offset: usize, + pub limit: usize, + pub total_hits: usize, + } + + let response = http::fetch_json::(Method::GET, url, None, None).await?; + + Ok(Paginated { + offset: response.offset, + limit: response.limit, + total: response.total_hits, + items: response.hits, + }) } async fn get(&self, slug: &str) -> LauncherResult { @@ -250,6 +303,17 @@ impl ProviderExt for ModrinthProviderImpl { } } +fn package_type_to_string(package_type: &PackageType) -> String { + match package_type { + PackageType::Mod => "mod", + PackageType::ResourcePack => "resourcepack", + PackageType::Shader => "shader", + PackageType::DataPack => "datapack", + PackageType::ModPack => "modpack", + } + .to_string() +} + #[serde_as] #[derive(Deserialize)] struct ModrinthPackage { @@ -471,3 +535,77 @@ impl From for ManagedUser { } } } + +#[allow(dead_code)] +enum FacetOperation { + NotEq, + LargeEq, + Large, + SmallEq, + Small, + Eq, +} + +impl std::fmt::Display for FacetOperation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::NotEq => "!=", + Self::LargeEq => ">=", + Self::Large => ">", + Self::SmallEq => "<=", + Self::Small => "<", + Self::Eq => "=", + }) + } +} + +struct Facet(pub String, pub FacetOperation, pub String); + +impl std::fmt::Display for Facet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(format!("{}{}{}", self.0, self.1, self.2).as_str()) + } +} + +struct FacetBuilder { + pub facets: Vec>, +} + +impl FacetBuilder { + pub const fn builder() -> Self { + Self { facets: vec![] } + } + + pub fn and(&mut self, facet: Facet) -> &Self { + self.facets.push(vec![facet]); + self + } + + #[allow(dead_code)] + pub fn or(&mut self, facet: Facet) -> &Self { + let mut last_facet = self.facets.pop().unwrap_or_default(); + last_facet.push(facet); + self.facets.push(last_facet); + self + } + + pub fn build(&self) -> String { + let mut builder: Vec = vec![]; + + for facet in &self.facets { + let mut stringified = String::new(); + stringified.push('['); + for (i, f) in facet.iter().enumerate() { + if i != 0 { + stringified.push(','); + } + stringified.push_str(format!("\"{f}\"").as_str()); + } + stringified.push(']'); + + builder.push(stringified); + } + + builder.join(",") + } +} diff --git a/packages/core/src/api/tauri/commands.rs b/packages/core/src/api/tauri/commands.rs index 348b8f54..2ac6b8a4 100644 --- a/packages/core/src/api/tauri/commands.rs +++ b/packages/core/src/api/tauri/commands.rs @@ -1,8 +1,7 @@ use interpulse::api::minecraft::Version; -use onelauncher_entity::{clusters, icon::Icon, loader::GameLoader, package::Provider, packages, prelude::entity, resolution::Resolution}; +use onelauncher_entity::{icon::Icon, loader::GameLoader, package::Provider, packages, resolution::Resolution}; use sea_orm::ActiveValue::Set; -use serde::Serialize; -use crate::{api::packages::{data::{ManagedPackage, ManagedUser, ManagedVersion, SearchQuery}, provider::ProviderExt}, store::{Settings, State}, utils::pagination::Paginated}; +use crate::{api::packages::{data::{SearchResult, ManagedPackage, ManagedUser, ManagedVersion, SearchQuery}, provider::ProviderExt}, store::{Settings, State}, utils::pagination::Paginated}; use tauri::{AppHandle, Runtime}; use crate::{api::{self, cluster::dao::ClusterId}, error::{LauncherError, LauncherResult}, store::{credentials::MinecraftCredentials, Core}}; @@ -84,7 +83,7 @@ pub trait TauriLauncherApi { // Packages #[taurpc(alias = "searchPackages")] - async fn search_packages(provider: Provider, query: SearchQuery) -> LauncherResult>; + async fn search_packages(provider: Provider, query: SearchQuery) -> LauncherResult>; #[taurpc(alias = "getPackage")] async fn get_package(provider: Provider, slug: String) -> LauncherResult; @@ -390,7 +389,7 @@ impl TauriLauncherApi for TauriLauncherApiImpl { } // Packages - async fn search_packages(self, provider: Provider, query: SearchQuery) -> LauncherResult> { + async fn search_packages(self, provider: Provider, query: SearchQuery) -> LauncherResult> { Ok(provider.search(&query).await?) } diff --git a/packages/core/src/utils/pagination.rs b/packages/core/src/utils/pagination.rs index e9b9d44c..467aafe9 100644 --- a/packages/core/src/utils/pagination.rs +++ b/packages/core/src/utils/pagination.rs @@ -5,14 +5,14 @@ use serde::{Deserialize, Serialize}; #[onelauncher_macro::specta] #[derive(Clone, Serialize, Deserialize)] pub struct Paginated -where T: Clone + Serialize + Send { +where T: Clone + Serialize + Send + Sync { pub total: usize, pub offset: usize, pub limit: usize, pub items: Vec, } -impl Debug for Paginated { +impl Debug for Paginated { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Paginated") .field("total", &self.total) @@ -23,7 +23,7 @@ impl Debug for Paginated { } } -impl Paginated { +impl Paginated { #[must_use] pub const fn new(total: usize, offset: usize, limit: usize, items: Vec) -> Self { Self { total, offset, limit, items }