From 170762fc7b77e40dab96a9e0e44f7a7e85960a35 Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:13:12 +0200 Subject: [PATCH] feat: basic modrinth api implementation --- apps/desktop/src-tauri/src/api/commands.rs | 8 +- .../src/ui/hooks/useBrowserController.tsx | 20 ++ apps/desktop/src/ui/pages/Browser.tsx | 10 - .../src/ui/pages/browser/BrowserMain.tsx | 47 ++- crates/core/src/api/package/content/mod.rs | 76 +++++ .../core/src/api/package/content/modrinth.rs | 273 ++++++++++++++++++ crates/core/src/api/package/from.rs | 2 +- crates/core/src/api/package/mod.rs | 1 + crates/core/src/game/mod.rs | 2 + crates/core/src/store/package.rs | 28 +- crates/testing/src/main.rs | 47 ++- 11 files changed, 465 insertions(+), 49 deletions(-) create mode 100644 apps/desktop/src/ui/hooks/useBrowserController.tsx delete mode 100644 apps/desktop/src/ui/pages/Browser.tsx create mode 100644 crates/core/src/api/package/content/mod.rs create mode 100644 crates/core/src/api/package/content/modrinth.rs diff --git a/apps/desktop/src-tauri/src/api/commands.rs b/apps/desktop/src-tauri/src/api/commands.rs index 625f7a80..71d5ce22 100644 --- a/apps/desktop/src-tauri/src/api/commands.rs +++ b/apps/desktop/src-tauri/src/api/commands.rs @@ -246,10 +246,6 @@ pub async fn remove_user(uuid: Uuid) -> Result<(), String> { // #[specta::specta] // #[tauri::command] -// pub async fn import_instance() -> Result<(), String> { -// onelauncher::api::package::import::import_instance() +// pub async fn searchMods(query: String) -> Result, String> { +// // Ok(onelauncher::api::) // } - -// #[specta::specta] -// #[tauri::command] -// pub async fn diff --git a/apps/desktop/src/ui/hooks/useBrowserController.tsx b/apps/desktop/src/ui/hooks/useBrowserController.tsx new file mode 100644 index 00000000..6cdc060e --- /dev/null +++ b/apps/desktop/src/ui/hooks/useBrowserController.tsx @@ -0,0 +1,20 @@ +import { type Context, type ParentProps, createContext, useContext } from 'solid-js'; + +interface BrowserContextType { + getMod: (id: string) => void; + getMods: (category: string) => void; +}; + +const BrowserContext = createContext() as Context; + +export function BrowserContextProvider(props: ParentProps) { + return ( + + {props.children} + + ); +} + +export default function useBrowserController() { + return useContext(BrowserContext); +} diff --git a/apps/desktop/src/ui/pages/Browser.tsx b/apps/desktop/src/ui/pages/Browser.tsx deleted file mode 100644 index a7766200..00000000 --- a/apps/desktop/src/ui/pages/Browser.tsx +++ /dev/null @@ -1,10 +0,0 @@ -function BrowserPage() { - return ( -
-

Browser

- -
- ); -} - -export default BrowserPage; diff --git a/apps/desktop/src/ui/pages/browser/BrowserMain.tsx b/apps/desktop/src/ui/pages/browser/BrowserMain.tsx index dcbc29b3..028050fb 100644 --- a/apps/desktop/src/ui/pages/browser/BrowserMain.tsx +++ b/apps/desktop/src/ui/pages/browser/BrowserMain.tsx @@ -67,14 +67,9 @@ function BrowserMain() {
- {/* eslint-disable-next-line ts/no-use-before-define -- blh */} {row => ( - <> - - - - + )}
@@ -83,27 +78,27 @@ function BrowserMain() { ); } -export const modStore: Mod[] = [ - { - id: '1yIQcc2b', - name: 'EvergreenHUD', - description: 'Improves your heads up display.', - author: 'Polyfrost', - icon_url: 'https://cdn.modrinth.com/data/1yIQcc2b/icon.png', - page_url: 'https://modrinth.com/mod/evergreenhud', - provider: Provider.Modrinth, - downloads: 281700, - ratings: 220, - }, -]; +// export const modStore: Mod[] = [ +// { +// id: '1yIQcc2b', +// name: 'EvergreenHUD', +// description: 'Improves your heads up display.', +// author: 'Polyfrost', +// icon_url: 'https://cdn.modrinth.com/data/1yIQcc2b/icon.png', +// page_url: 'https://modrinth.com/mod/evergreenhud', +// provider: Provider.Modrinth, +// downloads: 281700, +// ratings: 220, +// }, +// ]; -const _modRowData: ModsRowProps[] = [ - { - header: 'Highly Endorsed', - category: 'endorsed', - mods: modStore, - }, -]; +// const _modRowData: ModsRowProps[] = [ +// { +// header: 'Highly Endorsed', +// category: 'endorsed', +// mods: modStore, +// }, +// ]; interface ModsRowProps { header: string; diff --git a/crates/core/src/api/package/content/mod.rs b/crates/core/src/api/package/content/mod.rs new file mode 100644 index 00000000..f65a3e7d --- /dev/null +++ b/crates/core/src/api/package/content/mod.rs @@ -0,0 +1,76 @@ +//! **OneLauncher Content Package** +//! +//! Utilities for searching and downloading content packages to OneLauncher. + +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; + +use crate::{data::{ManagedPackage, ManagedVersion}, Result}; + +mod modrinth; + +/// Providers for content packages +#[cfg_attr(feature = "tauri", derive(specta::Type))] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] +pub enum Providers { + // Curseforge, + Modrinth, +} + +impl Providers { + /// Get the name of the provider + pub fn name(&self) -> &str { + match self { + // Providers::Curseforge => "Curseforge", + Providers::Modrinth => "Modrinth", + } + } + + /// Get the URL of the provider + pub fn url(&self) -> &str { + match self { + // Providers::Curseforge => "https://curseforge.com/", + Providers::Modrinth => "https://modrinth.com", + } + } + + pub async fn list(&self) -> Result> { + Ok(match self { + // Providers::Curseforge => curseforge::list(), + Providers::Modrinth => modrinth::list(), + }.await?.into_iter().map(|p| p.into()).collect()) + } + + pub async fn get(&self, slug_or_id: &str) -> Result { + Ok(match self { + // Providers::Curseforge => curseforge::get(slug), + Providers::Modrinth => modrinth::get(slug_or_id), + }.await?.into()) + } + + pub async fn get_versions(&self, project_id: &str) -> Result> { + Ok(match self { + // Providers::Curseforge => curseforge::get_versions(slug), + Providers::Modrinth => modrinth::get_versions(project_id), + }.await?.into_iter().map(|p| p.into()).collect()) + } + + pub async fn get_version(&self, version_id: &str) -> Result { + Ok(match self { + // Providers::Curseforge => curseforge::get_version(slug, version), + Providers::Modrinth => modrinth::get_version(version_id), + }.await?.into()) + } + + pub async fn get_version_for_game_version(&self, id: &str, game_version: &str) -> Result { + let versions = self.get_versions(id).await?; + Ok(versions.into_iter().find(|v| v.game_versions.contains(&game_version.to_string())).ok_or(anyhow!("no game version found"))?) // TODO: error handling + } + + // pub async fn search(&self, query: &str) -> Result> { + // match self { + // // Providers::Curseforge => curseforge::search(query), + // Providers::Modrinth => modrinth::search(query), + // }.await + // } +} \ No newline at end of file diff --git a/crates/core/src/api/package/content/modrinth.rs b/crates/core/src/api/package/content/modrinth.rs new file mode 100644 index 00000000..47ac7dcf --- /dev/null +++ b/crates/core/src/api/package/content/modrinth.rs @@ -0,0 +1,273 @@ +use crate::package::from::MODRINTH_API_URL; +use crate::store::{ManagedVersionFile, PackageFile, PackageSide}; +use crate::utils::http::fetch; +use crate::{State, Result}; +use crate::data::{ManagedPackage, ManagedVersion, PackageType}; + +use anyhow::anyhow; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModrinthPackage { + pub slug: String, + pub title: String, + #[serde(default)] + pub description: String, + #[serde(default)] + pub categories: Vec, + #[serde(rename = "client_side")] + pub client_side: PackageSide, + #[serde(rename = "server_side")] + pub server_side: PackageSide, + // #[serde(default)] + // pub body: String, + // #[serde(default)] + // pub status: String, + // #[serde(default)] + // #[serde(rename = "requested_status")] + // pub requested_status: String, + #[serde(default)] + #[serde(rename = "additional_categories")] + pub additional_categories: Vec, + // #[serde(rename = "issues_url")] + // pub issues_url: String, + // #[serde(rename = "source_url")] + // pub source_url: String, + // #[serde(rename = "wiki_url")] + // pub wiki_url: String, + // #[serde(rename = "discord_url")] + // pub discord_url: String, + // #[serde(rename = "donation_urls")] + // pub donation_urls: Vec, + // #[serde(rename = "project_type")] + // pub project_type: String, + pub downloads: u32, + #[serde(rename = "icon_url")] + pub icon_url: String, + // pub color: i64, + // #[serde(rename = "thread_id")] + // pub thread_id: String, + // #[serde(rename = "monetization_status")] + // pub monetization_status: String, + pub id: String, + // pub team: String, + // #[serde(rename = "body_url")] + // pub body_url: Value, + // #[serde(rename = "moderator_message")] + // pub moderator_message: Value, + pub published: DateTime, + pub updated: DateTime, + // pub approved: DateTime, + // pub queued: String, + pub followers: u32, + // pub license: License, + pub versions: Vec, + #[serde(rename = "game_versions")] + pub game_versions: Vec, + pub loaders: Vec, + // pub gallery: Vec, +} + +impl From for ManagedPackage { + fn from(value: ModrinthPackage) -> ManagedPackage { + ManagedPackage { + id: value.id, + title: value.title, + description: value.description, + main: value.slug, + versions: value.versions, + game_versions: value.game_versions, + loaders: value.loaders, + icon_url: Some(value.icon_url), + created: value.published, + updated: value.updated, + client: value.client_side, + server: value.server_side, + downloads: value.downloads, + followers: value.followers, + categories: value.categories, + optional_categories: Some(value.additional_categories), + uid: None, + package_type: PackageType::Mod, + } + } +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DonationUrl { + pub id: String, + pub platform: String, + pub url: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct License { + pub id: String, + pub name: String, + pub url: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Gallery { + pub url: String, + pub featured: bool, + pub title: String, + pub description: String, + pub created: String, + pub ordering: i64, +} + +macro_rules! format_url { + ($($arg:tt)*) => {{ + format!("{}{}", MODRINTH_API_URL, format!($($arg)*)) + }}; +} + +pub async fn list() -> Result> { + Ok(serde_json::from_slice( + &fetch( + format_url!("/projects_random?count=10").as_str(), + None, + &State::get().await?.fetch_semaphore + ).await? + )?) +} + +pub async fn get(id: &str) -> Result { + Ok(serde_json::from_slice( + &fetch( + format_url!("/project/{}", id).as_str(), + None, + &State::get().await?.fetch_semaphore + ).await? + )?) +} + +#[derive(Serialize, Deserialize)] +struct SearchResults { + hits: Vec, +} + +pub async fn search( + query: &str, +) -> Result> { + // TODO: Fix date_created and date_updated inconsistency (published and updated) + let response: SearchResults = serde_json::from_slice( + &fetch( + format_url!("/search?query={}", query).as_str(), + None, + &State::get().await?.fetch_semaphore + ).await?, + )?; + + Ok(response.hits) +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModrinthVersion { + #[serde(rename = "game_versions")] + pub game_versions: Vec, + pub loaders: Vec, + pub id: String, + #[serde(rename = "project_id")] + pub project_id: String, + #[serde(rename = "author_id")] + pub author_id: String, + pub featured: bool, + pub name: String, + #[serde(rename = "version_number")] + pub version_number: String, + pub changelog: String, + #[serde(rename = "changelog_url")] + pub changelog_url: Option, + #[serde(rename = "date_published")] + pub date_published: DateTime, + pub downloads: u32, + #[serde(rename = "version_type")] + pub version_type: String, + pub status: String, + #[serde(rename = "requested_status")] + pub requested_status: Value, + pub files: Vec, + pub dependencies: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct File { + pub url: String, + pub filename: String, + pub primary: bool, + pub size: u32, + #[serde(rename = "file_type")] + pub file_type: Option, +} + +impl From for ManagedVersionFile { + fn from(value: File) -> Self { + ManagedVersionFile { + url: value.url, + file_name: value.filename, + primary: value.primary, + size: value.size, + file_type: value.file_type, + hashes: Default::default(), // TODO + } + } +} + +impl From for ManagedVersion { + fn from(value: ModrinthVersion) -> Self { + ManagedVersion { + id: value.id, + package_id: value.project_id, + author: value.author_id, + name: value.name, + + featured: value.featured, + version_id: value.version_number, + changelog: value.changelog, + changelog_url: value.changelog_url, + + published: value.date_published, + downloads: value.downloads, + version_type: value.version_type, + + files: value.files.into_iter().map(|f| f.into()).collect(), + deps: vec![], // TODO [`ManagedDependency`]? + game_versions: value.game_versions, + loaders: value.loaders, + } + } +} + +pub async fn get_versions( + project_id: &str, +) -> Result> { + Ok(serde_json::from_slice( + &fetch( + format_url!("/project/{}/version", project_id).as_str(), + None, + &State::get().await?.fetch_semaphore + ).await? + )?) +} + +pub async fn get_version( + version_id: &str, +) -> Result { + Ok(serde_json::from_slice( + &fetch( + format_url!("/version/{}", version_id).as_str(), + None, + &State::get().await?.fetch_semaphore + ).await? + )?) +} \ No newline at end of file diff --git a/crates/core/src/api/package/from.rs b/crates/core/src/api/package/from.rs index 1de7c8a5..55f928ba 100644 --- a/crates/core/src/api/package/from.rs +++ b/crates/core/src/api/package/from.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; -pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/"; +pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2"; pub const CURSEFORGE_API_URL: &str = "https://api.cursefor"; #[derive(Serialize, Deserialize, Eq, PartialEq)] diff --git a/crates/core/src/api/package/mod.rs b/crates/core/src/api/package/mod.rs index 79f529a7..cb9ddd2d 100644 --- a/crates/core/src/api/package/mod.rs +++ b/crates/core/src/api/package/mod.rs @@ -6,3 +6,4 @@ pub mod from; pub mod import; pub mod mrpack; pub mod zip; +pub mod content; \ No newline at end of file diff --git a/crates/core/src/game/mod.rs b/crates/core/src/game/mod.rs index 18dbdfd2..5943f1f4 100644 --- a/crates/core/src/game/mod.rs +++ b/crates/core/src/game/mod.rs @@ -368,6 +368,8 @@ pub async fn launch_minecraft( ) .current_dir(instance_path.clone()); + println!("command: {:#?}", command); + // when cargo makes the DYLD_LIBRARY_PATH it breaks Minecraft #[cfg(target_os = "macos")] if std::env::var("CARGO").is_ok() { diff --git a/crates/core/src/store/package.rs b/crates/core/src/store/package.rs index 65abfc56..cc5916b8 100644 --- a/crates/core/src/store/package.rs +++ b/crates/core/src/store/package.rs @@ -10,7 +10,8 @@ use std::path::{Path, PathBuf}; use tokio::io::AsyncReadExt; use crate::prelude::IOError; -use crate::utils::http::{write_icon, FetchSemaphore, IoSemaphore}; +use crate::utils::http::{self, write_icon, FetchSemaphore, IoSemaphore}; +use crate::State; use super::{Cluster, PackagePath}; @@ -83,6 +84,7 @@ pub async fn generate_context( } /// Represents types of packages handled by the launcher. +#[cfg_attr(feature = "tauri", derive(specta::Type))] #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "lowercase")] pub enum PackageType { @@ -188,7 +190,7 @@ pub struct ManagedPackage { // Core Metadata pub id: String, pub uid: Option, - pub package_type: String, + pub package_type: PackageType, pub title: String, pub description: String, pub main: String, @@ -233,7 +235,7 @@ pub struct ManagedVersion { /// Universal interface for managed package files. #[cfg_attr(feature = "tauri", derive(specta::Type))] -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub struct ManagedVersionFile { pub url: String, pub file_name: String, @@ -244,6 +246,24 @@ pub struct ManagedVersionFile { pub hashes: HashMap, } +impl ManagedVersionFile { + #[tracing::instrument] + pub async fn download_to_cluster( + &self, + cluster: &Cluster, + ) -> crate::Result<()> { + tracing::info!("downloading mod '{}' to cluster '{}'", self.file_name, cluster.meta.name); + let path = cluster.get_full_path().await?.join("mods").join(&self.file_name); + let state = State::get().await?; + + // TODO: Implement hashes + let bytes = http::fetch(&self.url, None, &state.fetch_semaphore).await?; + http::write(&path, &bytes, &state.io_semaphore).await?; + + Ok(()) + } +} + /// Universal interface for managed package dependencies. #[cfg_attr(feature = "tauri", derive(specta::Type))] #[derive(Serialize, Deserialize, Clone, Debug)] @@ -290,7 +310,7 @@ pub enum PackageSide { /// The file type of a [`Package`]. #[cfg_attr(feature = "tauri", derive(specta::Type))] -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] #[serde(rename_all = "snake_case")] pub enum PackageFile { RequiredPack, diff --git a/crates/testing/src/main.rs b/crates/testing/src/main.rs index 206b832c..dd98567b 100644 --- a/crates/testing/src/main.rs +++ b/crates/testing/src/main.rs @@ -4,6 +4,7 @@ )] use onelauncher::cluster::create::create_cluster; +use onelauncher::package::content::{self, Providers}; use onelauncher::prelude::*; pub async fn authenticate_mc() -> onelauncher::Result { @@ -39,7 +40,49 @@ async fn main() -> onelauncher::Result<()> { let _logger = onelauncher::start_logger(); let state = State::get().await?; - if minecraft::users().await?.is_empty() { + modrinth().await?; + + Ok(()) +} + +async fn modrinth() -> onelauncher::Result<()> { + let state = State::get().await?; + + let provider = Providers::Modrinth; + + let result = provider.get("oneconfig").await?; + println!("{:#?}", result); + + let managed_version = provider.get_version_for_game_version(&result.id, "1.8.9").await?; + println!("{:#?}", result); + + let name = "forge 1.8.9 oneconfig".to_string(); + let game = "1.8.9".to_string(); + let loader = Loader::Forge; + + let cluster_path = create_cluster( + name.clone(), + game, + loader, + None, // latest + None, + None, + None, + None, + None, + ) + .await?; + + let cluster = cluster::get(&cluster_path, None).await?.expect("couldnt get cluster"); + managed_version.files.first().expect("no files found").download_to_cluster(&cluster).await?; + + Ok(()) +} + +async fn launch_and_authenticate() -> onelauncher::Result<()> { + let state = State::get().await?; + + if minecraft::users().await?.is_empty() { println!("authenticating"); authenticate_mc().await?; } @@ -94,5 +137,5 @@ async fn main() -> onelauncher::Result<()> { let mut proc = c_lock.write().await; processor::wait_for(&mut proc).await?; - Ok(()) + Ok(()) }