feat: modrinth search implementation

This commit is contained in:
LynithDev 2025-07-16 21:27:01 +02:00
parent 089fbd3e26
commit 3fa8e7f150
6 changed files with 203 additions and 21 deletions

View file

@ -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?;

View file

@ -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<String>,
pub offset: Option<usize>,
pub limit: Option<usize>,
pub sort: Option<String>,
pub sort: Option<Sort>,
pub filters: Option<Filters>,
}
#[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<String>,
pub display_categories: Vec<String>,
pub versions: Vec<String>,
pub downloads: usize,
pub icon_url: String,
pub date_created: DateTime<Utc>,
pub date_modified: DateTime<Utc>,
pub latest_version: String,
pub license: String,
pub client_side: PackageSide,
pub server_side: PackageSide,
/// List of URLs to images
pub gallery: Vec<String>,
// pub color: i64,
}
#[onelauncher_macro::specta]
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]

View file

@ -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<Paginated<ManagedPackage>>;
async fn search(&self, query: &SearchQuery) -> LauncherResult<Paginated<SearchResult>>;
async fn get(&self, slug: &str) -> LauncherResult<ManagedPackage>;
async fn get_multiple(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedPackage>>;
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<Paginated<ManagedPackage>> {
async fn search(&self, query: &SearchQuery) -> LauncherResult<Paginated<SearchResult>> {
match self {
Self::Modrinth => ModrinthProviderImpl.search(query).await,
_ => todo!("unimplemented provider"),

View file

@ -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<Paginated<ManagedPackage>> {
async fn search(&self, query: &SearchQuery) -> LauncherResult<Paginated<SearchResult>> {
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<SearchResult>,
pub offset: usize,
pub limit: usize,
pub total_hits: usize,
}
let response = http::fetch_json::<SearchResults>(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<ManagedPackage> {
@ -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<ModrinthUser> 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<Vec<Facet>>,
}
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<String> = 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(",")
}
}

View file

@ -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<Paginated<ManagedPackage>>;
async fn search_packages(provider: Provider, query: SearchQuery) -> LauncherResult<Paginated<SearchResult>>;
#[taurpc(alias = "getPackage")]
async fn get_package(provider: Provider, slug: String) -> LauncherResult<ManagedPackage>;
@ -390,7 +389,7 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
}
// Packages
async fn search_packages(self, provider: Provider, query: SearchQuery) -> LauncherResult<Paginated<ManagedPackage>> {
async fn search_packages(self, provider: Provider, query: SearchQuery) -> LauncherResult<Paginated<SearchResult>> {
Ok(provider.search(&query).await?)
}

View file

@ -5,14 +5,14 @@ use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta]
#[derive(Clone, Serialize, Deserialize)]
pub struct Paginated<T>
where T: Clone + Serialize + Send {
where T: Clone + Serialize + Send + Sync {
pub total: usize,
pub offset: usize,
pub limit: usize,
pub items: Vec<T>,
}
impl<T: Clone + Serialize + Send> Debug for Paginated<T> {
impl<T: Clone + Serialize + Send + Sync> Debug for Paginated<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Paginated")
.field("total", &self.total)
@ -23,7 +23,7 @@ impl<T: Clone + Serialize + Send> Debug for Paginated<T> {
}
}
impl <T: Clone + Serialize + Send> Paginated<T> {
impl <T: Clone + Serialize + Send + Sync> Paginated<T> {
#[must_use]
pub const fn new(total: usize, offset: usize, limit: usize, items: Vec<T>) -> Self {
Self { total, offset, limit, items }