feat: SkyClient Part 2

This commit is contained in:
LynithDev 2024-12-08 11:34:18 +01:00
parent f49dc71e9d
commit e973812aaf
10 changed files with 225 additions and 54 deletions

7
Cargo.lock generated
View file

@ -3212,6 +3212,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "md5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@ -3819,6 +3825,7 @@ dependencies = [
"interpulse", "interpulse",
"iota_stronghold", "iota_stronghold",
"lazy_static", "lazy_static",
"md5",
"murmur2", "murmur2",
"notify", "notify",
"notify-debouncer-mini", "notify-debouncer-mini",

View file

@ -134,6 +134,7 @@ flate2 = { version = "1.0" }
sha1_smol = { version = "1.0", features = [ "std" ] } sha1_smol = { version = "1.0", features = [ "std" ] }
sha2 = { version = "0.10" } sha2 = { version = "0.10" }
murmur2 = { version = "0.1.0" } murmur2 = { version = "0.1.0" }
md5 = { version = "0.7.0" }
bytes = { version = "1.7" } bytes = { version = "1.7" }
# # ============= UTILITY ============= # # # # ============= UTILITY ============= # #

View file

@ -865,7 +865,7 @@ export interface ManagedUser { id: string; username: string; url?: string | null
/** /**
* Universal managed package version of a package. * Universal managed package version of a package.
*/ */
export interface ManagedVersion { id: string; package_id: string; author: string; name: string; featured: boolean; version_display: string; changelog: string; changelog_url: string | null; published: string; downloads: number; version_type: ManagedVersionReleaseType; files: ManagedVersionFile[]; is_available: boolean; deps: ManagedDependency[]; game_versions: string[]; loaders: Loader[] }; export interface ManagedVersion { id: string; package_id: string; author: string; name: string; featured: boolean; version_display: string; changelog: string; changelog_url: string | null; published: string | null; downloads: number; version_type: ManagedVersionReleaseType; files: ManagedVersionFile[]; is_available: boolean; deps: ManagedDependency[]; game_versions: string[]; loaders: Loader[] };
/** /**
* Universal interface for managed package files. * Universal interface for managed package files.
*/ */

View file

@ -51,6 +51,7 @@ dirs = { workspace = true }
sha1_smol = { workspace = true } sha1_smol = { workspace = true }
sha2 = { workspace = true } sha2 = { workspace = true }
murmur2 = { workspace = true } murmur2 = { workspace = true }
md5 = { workspace = true }
async_zip = { workspace = true } async_zip = { workspace = true }
discord-rich-presence = { workspace = true } discord-rich-presence = { workspace = true }
regex = { workspace = true } regex = { workspace = true }

View file

@ -352,7 +352,7 @@ impl Into<ManagedVersion> for ModFile {
is_available: self.is_available && files.len() > 0, is_available: self.is_available && files.len() > 0,
files, files,
game_versions: game_versions, game_versions: game_versions,
published: self.file_date, published: Some(self.file_date),
version_display: self.display_name, version_display: self.display_name,
version_type: self.release_type.into(), version_type: self.release_type.into(),
} }

View file

@ -3,6 +3,7 @@
//! Utilities for searching and downloading content packages to `OneLauncher`. //! Utilities for searching and downloading content packages to `OneLauncher`.
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf;
use modrinth::{Facet, FacetOperation}; use modrinth::{Facet, FacetOperation};
use reqwest::Method; use reqwest::Method;
@ -11,6 +12,7 @@ use serde::{Deserialize, Serialize};
use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType}; use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType};
use crate::package::content::modrinth::FacetBuilder; use crate::package::content::modrinth::FacetBuilder;
use crate::store::{Author, PackageBody, ProviderSearchResults}; use crate::store::{Author, PackageBody, ProviderSearchResults};
use crate::utils::crypto;
use crate::utils::http::fetch_json; use crate::utils::http::fetch_json;
use crate::utils::pagination::Pagination; use crate::utils::pagination::Pagination;
use crate::{Result, State}; use crate::{Result, State};
@ -178,7 +180,10 @@ impl Providers {
(data.0.into_iter().map(Into::into).collect(), data.1) (data.0.into_iter().map(Into::into).collect(), data.1)
} }
Self::SkyClient => todo!(), Self::SkyClient => {
let data = skyclient::get_all_versions(project_id, game_versions, loaders, page, page_size).await?;
(data.0.into_iter().map(Into::into).collect(), data.1)
},
}) })
} }
@ -194,7 +199,7 @@ impl Providers {
.into_iter() .into_iter()
.map(Into::into) .map(Into::into)
.collect(), .collect(),
Self::SkyClient => todo!(), Self::SkyClient => skyclient::get_versions(versions).await?,
}) })
} }
@ -230,9 +235,20 @@ impl Providers {
.into_iter() .into_iter()
.map(|(hash, version)| (hash, version.into())) .map(|(hash, version)| (hash, version.into()))
.collect(), .collect(),
Self::SkyClient => todo!(), Self::SkyClient => skyclient::get_versions_by_hashes(hashes).await?,
}) })
} }
pub fn hash_file(&self, path: &PathBuf) -> Result<String> {
match self {
Providers::Modrinth => crypto::sha1_file(path),
Providers::SkyClient => crypto::md5_file(path),
Providers::Curseforge => {
let hash = crypto::murmur2_file(path)?;
Ok(hash.to_string())
},
}
}
} }
#[cfg_attr(feature = "specta", derive(specta::Type))] #[cfg_attr(feature = "specta", derive(specta::Type))]

View file

@ -177,7 +177,7 @@ impl From<ModrinthVersion> for ManagedVersion {
changelog: value.changelog, changelog: value.changelog,
changelog_url: value.changelog_url, changelog_url: value.changelog_url,
published: value.date_published, published: Some(value.date_published),
downloads: value.downloads, downloads: value.downloads,
version_type: ManagedVersionReleaseType::from(value.version_type), version_type: ManagedVersionReleaseType::from(value.version_type),

View file

@ -1,9 +1,9 @@
use std::sync::Arc; use std::{collections::HashMap, sync::Arc};
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tokio::sync::{OnceCell, RwLock}; use tokio::sync::{OnceCell, RwLock};
use crate::{data::{Loader, ManagedPackage, ManagedUser, ManagedVersion}, store::{ProviderSearchResults, SearchResult}, utils::{http, pagination::Pagination}, Result, State}; use crate::{data::{Loader, ManagedPackage, ManagedUser, ManagedVersion}, store::{ManagedVersionFile, ProviderSearchResults, SearchResult}, utils::{http, pagination::Pagination}, Result, State};
async fn fetch<T: DeserializeOwned>(url: &str) -> Result<T> { async fn fetch<T: DeserializeOwned>(url: &str) -> Result<T> {
let state = State::get().await?; let state = State::get().await?;
@ -143,6 +143,37 @@ pub struct SkyClientModVersion {
pub mod_id: Option<String>, pub mod_id: Option<String>,
} }
fn get_managed_version(m: &SkyClientMod, v: &SkyClientModVersion) -> ManagedVersion {
let mut map = HashMap::new();
map.insert(String::from("md5"), v.hash.clone());
ManagedVersion {
id: v.version.clone(),
package_id: m.id.clone(),
author: m.creator.clone(),
changelog_url: None,
changelog: String::new(),
deps: vec![],
downloads: 0,
featured: false,
files: vec![ManagedVersionFile {
file_name: v.file.clone(),
url: v.url.clone(),
file_type: None,
hashes: map,
primary: true,
size: 0,
}],
game_versions: v.game_versions.clone(),
is_available: true,
loaders: v.loaders.iter().map(|l| Loader::from_string(l.to_owned())).collect(),
name: format!("{} v{}", m.display, v.version),
published: None,
version_display: v.version.clone(),
version_type: crate::store::ManagedVersionReleaseType::Release,
}
}
pub async fn get(id: &str) -> Result<SkyClientMod> { pub async fn get(id: &str) -> Result<SkyClientMod> {
let store = SkyClientStore::get().await?; let store = SkyClientStore::get().await?;
@ -181,45 +212,97 @@ pub async fn get_multiple(slug_or_ids: &[String]) -> Result<Vec<SkyClientMod>> {
Ok(results) Ok(results)
} }
// pub async fn get_all_versions( pub async fn get_all_versions(
// project_id: &str, project_id: &str,
// game_versions: Option<Vec<String>>, game_versions: Option<Vec<String>>,
// loaders: Option<Vec<Loader>>, loaders: Option<Vec<Loader>>,
// page: Option<u32>, page: Option<u32>,
// page_size: Option<u16>, page_size: Option<u16>,
// ) -> Result<(Vec<ManagedVersion>, Pagination)> { ) -> Result<(Vec<ManagedVersion>, Pagination)> {
// let store = SkyClientStore::get().await?; let store = SkyClientStore::get().await?;
// let mods = match &store.mods { let mods = match &store.mods {
// Some(mods) => mods, Some(mods) => mods,
// None => return Ok((vec![], Pagination::default())) None => return Ok((vec![], Pagination::default()))
// }; };
let mut pagination = Pagination::default();
let mut versions = vec![];
// let mut versions = vec![]; for m in mods {
if m.id == project_id {
for v in &m.versions {
let mut can_add = true;
// for m in mods { if let Some(game_versions) = &game_versions {
// if m.id == project_id { if !game_versions.is_empty() {
// for v in m.versions { can_add = v.game_versions.iter().any(|gv| game_versions.contains(gv))
// let mut can_add = true; }
}
// if let Some(game_versions) = &game_versions { if can_add {
// can_add = v.game_versions.iter().any(|gv| game_versions.contains(gv)) if let Some(loaders) = &loaders {
// } if !loaders.is_empty() {
can_add = v.loaders.iter().any(|l| loaders.contains(&Loader::from_string(l.to_owned())))
}
}
}
// if can_add { if can_add {
// if let Some(loaders) = &loaders { if page_size.map(|ps| versions.len() < ps as usize).unwrap_or(true) && page.map(|p| p <= pagination.index).unwrap_or(true) {
// can_add = v.loaders.iter().any(|l| loaders.contains(&Loader::from_string(l))) versions.push(get_managed_version(m, v));
// } }
// }
// if can_add { pagination.total_count += 1;
// }
// } }
// } }
// } }
// }
// } Ok((versions, pagination))
}
pub async fn get_versions(versions: Vec<String>) -> Result<Vec<ManagedVersion>> {
let store = SkyClientStore::get().await?;
let mods = match &store.mods {
Some(mods) => mods,
None => return Ok(vec![])
};
let mut results = vec![];
for m in mods {
for v in &m.versions {
if versions.contains(&v.version) {
results.push(get_managed_version(m, v));
}
}
}
Ok(results)
}
pub async fn get_versions_by_hashes(hashes: Vec<String>) -> Result<HashMap<String, ManagedVersion>> {
let store = SkyClientStore::get().await?;
let mods = match &store.mods {
Some(mods) => mods,
None => return Ok(HashMap::new())
};
let mut results = HashMap::new();
for m in mods {
for v in &m.versions {
if hashes.contains(&v.hash) {
results.insert(v.hash.clone(), get_managed_version(m, v));
}
}
}
Ok(results)
}
pub async fn search( pub async fn search(
query: Option<String>, query: Option<String>,

View file

@ -11,6 +11,7 @@ use onelauncher_utils::io;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::vec;
use tokio::fs::DirEntry; use tokio::fs::DirEntry;
use super::{ClusterPath, Clusters, Directories, PackagePath}; use super::{ClusterPath, Clusters, Directories, PackagePath};
@ -471,21 +472,21 @@ impl PackageManager {
// Infer any packages // Infer any packages
if packages_to_infer.len() > 0 { if packages_to_infer.len() > 0 {
let not_found = infer_provider(Providers::Modrinth, package_type, packages, packages_to_infer, crypto::sha1_file).await; if let Some(not_found) = infer_provider(Providers::Modrinth, package_type, packages, packages_to_infer, crypto::sha1_file).await {
if let Some(not_found) = infer_provider(Providers::SkyClient, package_type, packages, not_found, crypto::md5_file).await {
if let Some(not_found) = infer_provider(Providers::Curseforge, package_type, packages, not_found, |path| {
let hash = crypto::murmur2_file(path)?;
Ok(hash.to_string())
}).await {
for path in not_found {
tracing::warn!("failed to infer package: {:?}", path);
if let Some(not_found) = not_found { let package_path = PackagePath::new(&path);
if let Some(not_found) = infer_provider(Providers::Curseforge, package_type, packages, not_found, |path| { let meta = PackageMetadata::Unknown;
let hash = crypto::murmur2_file(path)?; let package = Package::new(&package_path, meta)?;
Ok(hash.to_string())
}).await {
for path in not_found {
tracing::warn!("failed to infer package: {:?}", path);
let package_path = PackagePath::new(&path); packages.insert(package_path, package);
let meta = PackageMetadata::Unknown; }
let package = Package::new(&package_path, meta)?;
packages.insert(package_path, package);
} }
} }
} }
@ -529,6 +530,49 @@ impl PackageManager {
} }
} }
// #[tracing::instrument(skip(packages))]
// async fn infer_packages(
// packages: &mut PackagesMap,
// package_type: PackageType,
// to_infer: HashSet<PathBuf>,
// ) {
// let infer_order = vec![
// Providers::Modrinth,
// Providers::SkyClient,
// Providers::Curseforge,
// ];
// // if let Some(not_found) = infer_provider(Providers::Modrinth, package_type, packages, packages_to_infer, crypto::sha1_file).await {
// // if let Some(not_found) = infer_provider(Providers::SkyClient, package_type, packages, not_found, crypto::md5_file).await {
// // if let Some(not_found) = infer_provider(Providers::Curseforge, package_type, packages, not_found, |path| {
// // let hash = crypto::murmur2_file(path)?;
// // Ok(hash.to_string())
// // }).await {
// // }
// // }
// // }
// let mut to_infer = to_infer;
// for provider in infer_order {
// if let Some(not_found) = infer_provider(provider, package_type, packages, to_infer, move|path| {
// Ok(provider.hash_file(path))
// }).await {
// to_infer = not_found;
// }
// }
// for path in to_infer {
// tracing::warn!("failed to infer package: {:?}", path);
// let package_path = PackagePath::new(&path);
// let meta = PackageMetadata::Unknown;
// if let Ok(package) = Package::new(&package_path, meta) {
// packages.insert(package_path, package);
// }
// }
// }
#[tracing::instrument(skip(packages, to_infer))] #[tracing::instrument(skip(packages, to_infer))]
async fn infer_provider( async fn infer_provider(
provider: Providers, provider: Providers,
@ -537,6 +581,8 @@ async fn infer_provider(
to_infer: HashSet<PathBuf>, to_infer: HashSet<PathBuf>,
hash_fn: fn(&PathBuf) -> crate::Result<String>, hash_fn: fn(&PathBuf) -> crate::Result<String>,
) -> Option<HashSet<PathBuf>> { ) -> Option<HashSet<PathBuf>> {
// TODO: Add ingress
let mut hash_map = HashMap::<String, PathBuf>::new(); let mut hash_map = HashMap::<String, PathBuf>::new();
to_infer.iter().for_each(|path| { to_infer.iter().for_each(|path| {
if let Ok(hash) = hash_fn(path) { if let Ok(hash) = hash_fn(path) {
@ -756,7 +802,7 @@ pub struct ManagedVersion {
pub changelog: String, pub changelog: String,
pub changelog_url: Option<String>, pub changelog_url: Option<String>,
pub published: DateTime<Utc>, pub published: Option<DateTime<Utc>>,
pub downloads: u32, pub downloads: u32,
pub version_type: ManagedVersionReleaseType, pub version_type: ManagedVersionReleaseType,

View file

@ -18,6 +18,23 @@ pub fn sha1_files(files: Vec<&PathBuf>) -> Vec<Option<String>> {
hashes hashes
} }
pub fn mda5(bytes: &[u8]) -> String {
let digest = md5::compute(bytes);
format!("{:x}", digest)
}
pub fn md5_file(file: &PathBuf) -> Result<String> {
Ok(mda5(&std::fs::read(file)?))
}
pub fn md5_files(files: Vec<&PathBuf>) -> Vec<Option<String>> {
let mut hashes = Vec::new();
for file in files {
hashes.push(md5_file(file).ok());
}
hashes
}
const CURSEFORGE_FINGERPRINT_SEED: u32 = 1; const CURSEFORGE_FINGERPRINT_SEED: u32 = 1;
pub fn murmur2(bytes: &[u8]) -> u32 { pub fn murmur2(bytes: &[u8]) -> u32 {
let normalized = normalize_byte_array(bytes); let normalized = normalize_byte_array(bytes);