feat: basic modrinth api implementation

This commit is contained in:
LynithDev 2024-07-24 14:13:12 +02:00
parent 56df69f911
commit 170762fc7b
11 changed files with 465 additions and 49 deletions

View file

@ -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<Vec<ModrinthMod>, String> {
// // Ok(onelauncher::api::)
// }
// #[specta::specta]
// #[tauri::command]
// pub async fn

View file

@ -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<BrowserContextType>;
export function BrowserContextProvider(props: ParentProps) {
return (
<BrowserContext.Provider value={{}}>
{props.children}
</BrowserContext.Provider>
);
}
export default function useBrowserController() {
return useContext(BrowserContext);
}

View file

@ -1,10 +0,0 @@
function BrowserPage() {
return (
<div class="flex flex-col gap-y-4">
<h1>Browser</h1>
</div>
);
}
export default BrowserPage;

View file

@ -67,14 +67,9 @@ function BrowserMain() {
</div>
<div class="flex flex-col h-full flex-1 gap-4 py-2">
{/* eslint-disable-next-line ts/no-use-before-define -- blh */}
<For each={_modRowData}>
{row => (
<>
<ModsRow {...row} />
<ModsRow {...row} />
<ModsRow {...row} />
</>
<ModsRow {...row} />
)}
</For>
</div>
@ -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;

View file

@ -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<Vec<ManagedPackage>> {
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<ManagedPackage> {
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<Vec<ManagedVersion>> {
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<ManagedVersion> {
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<ManagedVersion> {
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<Vec<ManagedPackage>> {
// match self {
// // Providers::Curseforge => curseforge::search(query),
// Providers::Modrinth => modrinth::search(query),
// }.await
// }
}

View file

@ -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<String>,
#[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<String>,
// #[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<DonationUrl>,
// #[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<Utc>,
pub updated: DateTime<Utc>,
// pub approved: DateTime<Utc>,
// pub queued: String,
pub followers: u32,
// pub license: License,
pub versions: Vec<String>,
#[serde(rename = "game_versions")]
pub game_versions: Vec<String>,
pub loaders: Vec<String>,
// pub gallery: Vec<Gallery>,
}
impl From<ModrinthPackage> 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<Vec<ModrinthPackage>> {
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<ModrinthPackage> {
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<ModrinthPackage>,
}
pub async fn search(
query: &str,
) -> Result<Vec<ModrinthPackage>> {
// 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<String>,
pub loaders: Vec<String>,
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<String>,
#[serde(rename = "date_published")]
pub date_published: DateTime<Utc>,
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<File>,
pub dependencies: Vec<Value>,
}
#[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<PackageFile>,
}
impl From<File> 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<ModrinthVersion> 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<Vec<ModrinthVersion>> {
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<ModrinthVersion> {
Ok(serde_json::from_slice(
&fetch(
format_url!("/version/{}", version_id).as_str(),
None,
&State::get().await?.fetch_semaphore
).await?
)?)
}

View file

@ -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)]

View file

@ -6,3 +6,4 @@ pub mod from;
pub mod import;
pub mod mrpack;
pub mod zip;
pub mod content;

View file

@ -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() {

View file

@ -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<String>,
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<String, String>,
}
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,

View file

@ -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<MinecraftCredentials> {
@ -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(())
}