You've already forked AstralRinth
forked from didirus/AstralRinth
Discord and playtime (#462)
* initial * Fixed java thing * fixes * internet check change * some fix/test commit * Fix render issues on windows * bump version --------- Co-authored-by: Jai A <jaiagr+gpg@pm.me> Co-authored-by: Jai A <jai@modrinth.com>
This commit is contained in:
@@ -6,7 +6,7 @@ use std::path::PathBuf;
|
||||
use crate::event::emit::{emit_loading, init_loading};
|
||||
use crate::state::CredentialsStore;
|
||||
use crate::util::fetch::{fetch_advanced, fetch_json};
|
||||
use crate::util::io;
|
||||
|
||||
use crate::util::jre::extract_java_majorminor_version;
|
||||
use crate::{
|
||||
state::JavaGlobals,
|
||||
@@ -117,10 +117,6 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
|
||||
let path = state.directories.java_versions_dir().await;
|
||||
|
||||
if path.exists() {
|
||||
io::remove_dir_all(&path).await?;
|
||||
}
|
||||
|
||||
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file))
|
||||
.map_err(|_| {
|
||||
crate::Error::from(crate::ErrorKind::InputError(
|
||||
|
||||
@@ -273,9 +273,7 @@ pub async fn install_zipped_mrpack_files(
|
||||
profile::edit_icon(&profile_path, Some(&potential_icon)).await?;
|
||||
}
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
if let Some(profile_val) = profile::get(&profile_path, None).await? {
|
||||
crate::launcher::install_minecraft(&profile_val, Some(loading_bar))
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::pack::install_from::{
|
||||
use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
|
||||
use crate::state::ProjectMetadata;
|
||||
|
||||
use crate::util::fetch;
|
||||
use crate::util::io::{self, IOError};
|
||||
use crate::{
|
||||
auth::{self, refresh},
|
||||
@@ -22,6 +23,7 @@ pub use crate::{
|
||||
};
|
||||
use async_zip::tokio::write::ZipFileWriter;
|
||||
use async_zip::{Compression, ZipEntryBuilder};
|
||||
use serde_json::json;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -878,6 +880,65 @@ pub async fn run_credentials(
|
||||
Ok(mc_process)
|
||||
}
|
||||
|
||||
/// Update playtime- sending a request to the server to update the playtime
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn try_update_playtime(path: &ProfilePathId) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let profile = get(path, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to update playtime for a nonexistent or unloaded profile at path {}!",
|
||||
path
|
||||
))
|
||||
})?;
|
||||
let updated_recent_playtime = profile.metadata.recent_time_played;
|
||||
|
||||
let res = if updated_recent_playtime > 0 {
|
||||
// Create update struct to send to Labrinth
|
||||
let modrinth_pack_version_id =
|
||||
profile.metadata.linked_data.and_then(|l| l.version_id);
|
||||
let playtime_update_json = json!({
|
||||
"seconds": updated_recent_playtime,
|
||||
"loader": profile.metadata.loader.to_string(),
|
||||
"game_version": profile.metadata.game_version,
|
||||
"parent": modrinth_pack_version_id,
|
||||
});
|
||||
// Copy this struct for every Modrinth project in the profile
|
||||
let mut hashmap: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
for (_, project) in profile.projects {
|
||||
if let ProjectMetadata::Modrinth { version, .. } = project.metadata
|
||||
{
|
||||
hashmap.insert(version.id, playtime_update_json.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let creds = state.credentials.read().await;
|
||||
fetch::post_json(
|
||||
"https://api.modrinth.com/analytics/playtime",
|
||||
serde_json::to_value(hashmap)?,
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
|
||||
// If successful, update the profile metadata to match submitted
|
||||
if res.is_ok() {
|
||||
let mut profiles = state.profiles.write().await;
|
||||
if let Some(profile) = profiles.0.get_mut(path) {
|
||||
profile.metadata.submitted_time_played += updated_recent_playtime;
|
||||
profile.metadata.recent_time_played = 0;
|
||||
}
|
||||
}
|
||||
// Sync either way
|
||||
State::sync().await?;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn get_modrinth_pack_list(packfile: &PackFormat) -> Vec<String> {
|
||||
packfile
|
||||
.files
|
||||
|
||||
@@ -103,6 +103,7 @@ pub async fn install_minecraft(
|
||||
profile: &Profile,
|
||||
existing_loading_bar: Option<LoadingBarId>,
|
||||
) -> crate::Result<()> {
|
||||
let sync_projects = existing_loading_bar.is_some();
|
||||
let loading_bar = init_or_edit_loading(
|
||||
existing_loading_bar,
|
||||
LoadingBarType::MinecraftDownload {
|
||||
@@ -123,6 +124,10 @@ pub async fn install_minecraft(
|
||||
.await?;
|
||||
State::sync().await?;
|
||||
|
||||
if sync_projects {
|
||||
Profile::sync_projects_task(profile.profile_id(), true);
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let instance_path =
|
||||
&io::canonicalize(&profile.get_profile_full_path().await?)?;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::{Profile, ProfilePathId};
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitStatus;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
@@ -12,6 +13,7 @@ use tracing::error;
|
||||
|
||||
use crate::event::emit::emit_process;
|
||||
use crate::event::ProcessPayloadType;
|
||||
use crate::profile;
|
||||
use crate::util::io::IOError;
|
||||
|
||||
use tokio::task::JoinHandle;
|
||||
@@ -29,6 +31,7 @@ pub struct MinecraftChild {
|
||||
pub manager: Option<JoinHandle<crate::Result<ExitStatus>>>, // None when future has completed and been handled
|
||||
pub current_child: Arc<RwLock<Child>>,
|
||||
pub output: SharedOutput,
|
||||
pub last_updated_playtime: DateTime<Utc>, // The last time we updated the playtime for the associated profile
|
||||
}
|
||||
|
||||
impl Children {
|
||||
@@ -94,6 +97,7 @@ impl Children {
|
||||
post_command,
|
||||
pid,
|
||||
current_child.clone(),
|
||||
profile_relative_path.clone(),
|
||||
)));
|
||||
|
||||
emit_process(
|
||||
@@ -104,6 +108,8 @@ impl Children {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let last_updated_playtime = Utc::now();
|
||||
|
||||
// Create MinecraftChild
|
||||
let mchild = MinecraftChild {
|
||||
uuid,
|
||||
@@ -111,6 +117,7 @@ impl Children {
|
||||
current_child,
|
||||
output: shared_output,
|
||||
manager,
|
||||
last_updated_playtime,
|
||||
};
|
||||
|
||||
let mchild = Arc::new(RwLock::new(mchild));
|
||||
@@ -128,11 +135,13 @@ impl Children {
|
||||
post_command: Option<Command>,
|
||||
mut current_pid: u32,
|
||||
current_child: Arc<RwLock<Child>>,
|
||||
associated_profile: ProfilePathId,
|
||||
) -> crate::Result<ExitStatus> {
|
||||
let current_child = current_child.clone();
|
||||
|
||||
// Wait on current Minecraft Child
|
||||
let mut mc_exit_status;
|
||||
let mut last_updated_playtime = Utc::now();
|
||||
loop {
|
||||
if let Some(t) = current_child
|
||||
.write()
|
||||
@@ -145,8 +154,61 @@ impl Children {
|
||||
}
|
||||
// sleep for 10ms
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
|
||||
// Auto-update playtime every minute
|
||||
let diff = Utc::now()
|
||||
.signed_duration_since(last_updated_playtime)
|
||||
.num_seconds();
|
||||
if diff >= 60 {
|
||||
if let Err(e) =
|
||||
profile::edit(&associated_profile, |mut prof| {
|
||||
prof.metadata.recent_time_played += diff as u64;
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"Failed to update playtime for profile {}: {}",
|
||||
associated_profile,
|
||||
e
|
||||
);
|
||||
}
|
||||
last_updated_playtime = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
// Now fully complete- update playtime one last time
|
||||
let diff = Utc::now()
|
||||
.signed_duration_since(last_updated_playtime)
|
||||
.num_seconds();
|
||||
if let Err(e) = profile::edit(&associated_profile, |mut prof| {
|
||||
prof.metadata.recent_time_played += diff as u64;
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"Failed to update playtime for profile {}: {}",
|
||||
associated_profile,
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
// Publish play time update
|
||||
// Allow failure, it will be stored locally and sent next time
|
||||
// Sent in another thread as first call may take a couple seconds and hold up process ending
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
profile::try_update_playtime(&associated_profile).await
|
||||
{
|
||||
tracing::warn!(
|
||||
"Failed to update playtime for profile {}: {}",
|
||||
associated_profile,
|
||||
e
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
// Clear game played for Discord RPC
|
||||
// May have other active processes, so we clear to the next running process
|
||||
|
||||
@@ -99,6 +99,7 @@ impl DiscordGuard {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*
|
||||
/// Clear the activity
|
||||
pub async fn clear_activity(
|
||||
&self,
|
||||
@@ -137,7 +138,7 @@ impl DiscordGuard {
|
||||
res.map_err(could_not_clear_err)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}*/
|
||||
|
||||
/// Clear the activity, but if there is a running profile, set the activity to that instead
|
||||
pub async fn clear_to_default(
|
||||
@@ -160,7 +161,7 @@ impl DiscordGuard {
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
self.clear_activity(reconnect_if_fail).await?;
|
||||
self.set_activity("Idling...", reconnect_if_fail).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ impl State {
|
||||
)));
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
|
||||
let is_offline = !fetch::check_internet(&fetch_semaphore, 3).await;
|
||||
let is_offline = !fetch::check_internet(3).await;
|
||||
|
||||
let metadata_fut =
|
||||
Metadata::init(&directories, !is_offline, &io_semaphore);
|
||||
@@ -185,6 +185,10 @@ impl State {
|
||||
let safety_processes = SafeProcesses::new();
|
||||
|
||||
let discord_rpc = DiscordGuard::init().await?;
|
||||
{
|
||||
// Add default Idling to discord rich presence
|
||||
let _ = discord_rpc.set_activity("Idling...", true).await;
|
||||
}
|
||||
|
||||
// Starts a loop of checking if we are online, and updating
|
||||
Self::offine_check_loop();
|
||||
@@ -323,7 +327,7 @@ impl State {
|
||||
|
||||
/// Refreshes whether or not the launcher should be offline, by whether or not there is an internet connection
|
||||
pub async fn refresh_offline(&self) -> crate::Result<()> {
|
||||
let is_online = fetch::check_internet(&self.fetch_semaphore, 3).await;
|
||||
let is_online = fetch::check_internet(3).await;
|
||||
|
||||
let mut offline = self.offline.write().await;
|
||||
|
||||
@@ -341,7 +345,7 @@ pub async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
let (mut tx, mut rx) = channel(1);
|
||||
|
||||
let file_watcher = new_debouncer(
|
||||
Duration::from_secs_f32(0.25),
|
||||
Duration::from_secs_f32(2.0),
|
||||
None,
|
||||
move |res: DebounceEventResult| {
|
||||
futures::executor::block_on(async {
|
||||
@@ -394,7 +398,10 @@ pub async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
Profile::crash_task(profile_path_id);
|
||||
} else if !visited_paths.contains(&new_path) {
|
||||
if subfile {
|
||||
Profile::sync_projects_task(profile_path_id);
|
||||
Profile::sync_projects_task(
|
||||
profile_path_id,
|
||||
false,
|
||||
);
|
||||
visited_paths.push(new_path);
|
||||
} else {
|
||||
Profiles::sync_available_profiles_task(
|
||||
|
||||
@@ -222,7 +222,7 @@ pub async fn login_password(
|
||||
) -> crate::Result<ModrinthCredentialsResult> {
|
||||
let resp = fetch_advanced(
|
||||
Method::POST,
|
||||
&format!("https://{MODRINTH_API_URL}auth/login"),
|
||||
&format!("{MODRINTH_API_URL}auth/login"),
|
||||
None,
|
||||
Some(serde_json::json!({
|
||||
"username": username,
|
||||
|
||||
@@ -183,6 +183,10 @@ pub struct ProfileMetadata {
|
||||
pub date_modified: DateTime<Utc>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_played: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub submitted_time_played: u64,
|
||||
#[serde(default)]
|
||||
pub recent_time_played: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
@@ -265,6 +269,8 @@ impl Profile {
|
||||
date_created: Utc::now(),
|
||||
date_modified: Utc::now(),
|
||||
last_played: None,
|
||||
submitted_time_played: 0,
|
||||
recent_time_played: 0,
|
||||
},
|
||||
projects: HashMap::new(),
|
||||
java: None,
|
||||
@@ -324,7 +330,7 @@ impl Profile {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn sync_projects_task(profile_path_id: ProfilePathId) {
|
||||
pub fn sync_projects_task(profile_path_id: ProfilePathId, force: bool) {
|
||||
tokio::task::spawn(async move {
|
||||
let span =
|
||||
tracing::span!(tracing::Level::INFO, "sync_projects_task");
|
||||
@@ -339,32 +345,34 @@ impl Profile {
|
||||
let profile = crate::api::profile::get(&profile_path_id, None).await?;
|
||||
|
||||
if let Some(profile) = profile {
|
||||
let paths = profile.get_profile_full_project_paths().await?;
|
||||
if profile.install_stage != ProfileInstallStage::PackInstalling || force {
|
||||
let paths = profile.get_profile_full_project_paths().await?;
|
||||
|
||||
let caches_dir = state.directories.caches_dir();
|
||||
let creds = state.credentials.read().await;
|
||||
let projects = crate::state::infer_data_from_files(
|
||||
profile.clone(),
|
||||
paths,
|
||||
caches_dir,
|
||||
&state.io_semaphore,
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
)
|
||||
.await?;
|
||||
drop(creds);
|
||||
let caches_dir = state.directories.caches_dir();
|
||||
let creds = state.credentials.read().await;
|
||||
let projects = crate::state::infer_data_from_files(
|
||||
profile.clone(),
|
||||
paths,
|
||||
caches_dir,
|
||||
&state.io_semaphore,
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
)
|
||||
.await?;
|
||||
drop(creds);
|
||||
|
||||
let mut new_profiles = state.profiles.write().await;
|
||||
if let Some(profile) = new_profiles.0.get_mut(&profile_path_id) {
|
||||
profile.projects = projects;
|
||||
let mut new_profiles = state.profiles.write().await;
|
||||
if let Some(profile) = new_profiles.0.get_mut(&profile_path_id) {
|
||||
profile.projects = projects;
|
||||
}
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
&profile_path_id,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Synced,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
&profile_path_id,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Synced,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Unable to fetch single profile projects: path {profile_path_id} invalid",
|
||||
@@ -980,7 +988,7 @@ impl Profiles {
|
||||
.await?,
|
||||
)
|
||||
.await?;
|
||||
Profile::sync_projects_task(profile_path_id);
|
||||
Profile::sync_projects_task(profile_path_id, false);
|
||||
}
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
|
||||
@@ -213,18 +213,41 @@ pub async fn fetch_mirrors(
|
||||
}
|
||||
|
||||
/// Using labrinth API, checks if an internet response can be found, with a timeout in seconds
|
||||
#[tracing::instrument(skip(semaphore))]
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn check_internet(semaphore: &FetchSemaphore, timeout: u64) -> bool {
|
||||
let result = fetch(
|
||||
"https://api.modrinth.com",
|
||||
None,
|
||||
semaphore,
|
||||
&CredentialsStore(None),
|
||||
);
|
||||
let result =
|
||||
tokio::time::timeout(Duration::from_secs(timeout), result).await;
|
||||
matches!(result, Ok(Ok(_)))
|
||||
pub async fn check_internet(timeout: u64) -> bool {
|
||||
REQWEST_CLIENT
|
||||
.get("https://launcher-files.modrinth.com/detect.txt")
|
||||
.timeout(Duration::from_secs(timeout))
|
||||
.send()
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Posts a JSON to a URL
|
||||
#[tracing::instrument(skip(json_body, semaphore))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn post_json<T>(
|
||||
url: &str,
|
||||
json_body: serde_json::Value,
|
||||
semaphore: &FetchSemaphore,
|
||||
credentials: &CredentialsStore,
|
||||
) -> crate::Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let io_semaphore = semaphore.0.read().await;
|
||||
let _permit = io_semaphore.acquire().await?;
|
||||
|
||||
let mut req = REQWEST_CLIENT.post(url).json(&json_body);
|
||||
if let Some(creds) = &credentials.0 {
|
||||
req = req.header("Authorization", &creds.session);
|
||||
}
|
||||
|
||||
let result = req.send().await?.error_for_status()?;
|
||||
|
||||
let value = result.json().await?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub async fn read_json<T>(
|
||||
|
||||
Reference in New Issue
Block a user