Merge branch 'feature/user-profile' into feature/refator-process-watcher-and-game-running

# Conflicts:
#	src/main/services/hydra-api.ts
#	src/main/services/process-watcher.ts
#	src/renderer/src/declaration.d.ts
This commit is contained in:
Zamitto 2024-06-19 21:05:22 -03:00
commit 800e99fda0
19 changed files with 283 additions and 18 deletions

View File

@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "1.2.4",
"version": "2.0.0",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",

View File

@ -22,6 +22,9 @@ export class Game {
@Column("text", { unique: true })
objectID: string;
@Column("text", { unique: true, nullable: true })
remoteId: string | null;
@Column("text")
title: string;

View File

@ -6,6 +6,7 @@ import type { GameShop } from "@types";
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
import { steamGamesWorker } from "@main/workers";
import { createGame } from "@main/services/library-sync";
const addGameToLibrary = async (
_event: Electron.IpcMainInvokeEvent,
@ -49,6 +50,21 @@ const addGameToLibrary = async (
}
});
}
const game = await gameRepository.findOne({ where: { objectID } });
createGame(game!).then((response) => {
const {
id: remoteId,
playTimeInMilliseconds,
lastTimePlayed,
} = response.data;
gameRepository.update(
{ objectID },
{ remoteId, playTimeInMilliseconds, lastTimePlayed }
);
});
});
};

View File

@ -1,5 +1,7 @@
import { registerEvent } from "../register-event";
import { gameRepository } from "../../repository";
import { HydraApi } from "@main/services/hydra-api";
import { logger } from "@main/services";
const removeGameFromLibrary = async (
_event: Electron.IpcMainInvokeEvent,
@ -9,6 +11,18 @@ const removeGameFromLibrary = async (
{ id: gameId },
{ isDeleted: true, executablePath: null }
);
removeRemoveGameFromLibrary(gameId).catch((err) => {
logger.error("removeRemoveGameFromLibrary", err);
});
};
const removeRemoveGameFromLibrary = async (gameId: number) => {
const game = await gameRepository.findOne({ where: { id: gameId } });
if (game?.remoteId) {
HydraApi.delete(`/games/${game.remoteId}`);
}
};
registerEvent("removeGameFromLibrary", removeGameFromLibrary);

View File

@ -12,6 +12,7 @@ import { DownloadManager } from "@main/services";
import { Not } from "typeorm";
import { steamGamesWorker } from "@main/workers";
import { createGame } from "@main/services/library-sync";
const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
@ -94,6 +95,19 @@ const startGameDownload = async (
},
});
createGame(updatedGame!).then((response) => {
const {
id: remoteId,
playTimeInMilliseconds,
lastTimePlayed,
} = response.data;
gameRepository.update(
{ objectID },
{ remoteId, playTimeInMilliseconds, lastTimePlayed }
);
});
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });

View File

@ -10,6 +10,7 @@ import { fetchDownloadSourcesAndUpdate } from "./helpers";
import { publishNewRepacksNotifications } from "./services/notifications";
import { MoreThan } from "typeorm";
import { HydraApi } from "./services/hydra-api";
import { uploadGamesBatch } from "./services/library-sync";
startMainLoop();
@ -21,7 +22,9 @@ const loadState = async (userPreferences: UserPreferences | null) => {
if (userPreferences?.realDebridApiToken)
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
HydraApi.setupApi();
HydraApi.setupApi().then(async () => {
if (HydraApi.isLoggedIn()) uploadGamesBatch();
});
const [nextQueueItem] = await downloadQueueRepository.find({
order: {

View File

@ -2,6 +2,9 @@ import { userAuthRepository } from "@main/repository";
import axios, { AxiosError, AxiosInstance } from "axios";
import { WindowManager } from "./window-manager";
import url from "url";
import { uploadGamesBatch } from "./library-sync";
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
import { logger } from "./logger";
export class HydraApi {
private static instance: AxiosInstance;
@ -53,6 +56,8 @@ export class HydraApi {
if (WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send("on-signin");
await clearGamesRemoteIds();
uploadGamesBatch();
}
}
@ -63,20 +68,20 @@ export class HydraApi {
this.instance.interceptors.request.use(
(request) => {
console.log(" ---- REQUEST -----");
console.log(request.method, request.url, request.headers, request.data);
logger.log(" ---- REQUEST -----");
logger.log(request.method, request.url, request.data);
return request;
},
(error) => {
console.log("request error", error);
logger.log("request error", error);
return Promise.reject(error);
}
);
this.instance.interceptors.response.use(
(response) => {
console.log(" ---- RESPONSE -----");
console.log(
logger.log(" ---- RESPONSE -----");
logger.log(
response.status,
response.config.method,
response.config.url,
@ -85,7 +90,7 @@ export class HydraApi {
return response;
},
(error) => {
console.log("response error", error);
logger.error("response error", error);
return Promise.reject(error);
}
);
@ -102,7 +107,11 @@ export class HydraApi {
}
private static async revalidateAccessTokenIfExpired() {
if (!this.userAuth.authToken) throw new Error("user is not logged in");
if (!this.userAuth.authToken) {
userAuthRepository.delete({ id: 1 });
logger.error("user is not logged in");
throw new Error("user is not logged in");
}
const now = new Date();
if (this.userAuth.expirationTimestamp < now.getTime()) {
@ -145,6 +154,8 @@ export class HydraApi {
if (WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send("on-signout");
}
logger.log("user refresh token expired");
}
throw err;
@ -179,4 +190,9 @@ export class HydraApi {
await this.revalidateAccessTokenIfExpired();
return this.instance.patch(url, data, this.getAxiosConfig());
}
static async delete(url: string) {
await this.revalidateAccessTokenIfExpired();
return this.instance.delete(url, this.getAxiosConfig());
}
}

View File

@ -0,0 +1,5 @@
import { gameRepository } from "@main/repository";
export const clearGamesRemoteIds = () => {
return gameRepository.update({}, { remoteId: null });
};

View File

@ -0,0 +1,11 @@
import { Game } from "@main/entity";
import { HydraApi } from "../hydra-api";
export const createGame = async (game: Game) => {
return HydraApi.post(`/games`, {
objectId: game.objectID,
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
shop: game.shop,
lastTimePlayed: game.lastTimePlayed,
});
};

View File

@ -0,0 +1,4 @@
export * from "./merge-with-remote-games";
export * from "./upload-games-batch";
export * from "./update-game-playtime";
export * from "./create-game";

View File

@ -0,0 +1,69 @@
import { gameRepository } from "@main/repository";
import { HydraApi } from "../hydra-api";
import { steamGamesWorker } from "@main/workers";
import { getSteamAppAsset } from "@main/helpers";
import { logger } from "../logger";
import { AxiosError } from "axios";
export const mergeWithRemoteGames = async () => {
try {
const games = await HydraApi.get("/games");
for (const game of games.data) {
const localGame = await gameRepository.findOne({
where: {
objectID: game.objectId,
},
});
if (localGame) {
const updatedLastTimePlayed =
localGame.lastTimePlayed == null ||
new Date(game.lastTimePlayed) > localGame.lastTimePlayed
? new Date(game.lastTimePlayed)
: localGame.lastTimePlayed;
const updatedPlayTime =
localGame.playTimeInMilliseconds < game.playTimeInMilliseconds
? game.playTimeInMilliseconds
: localGame.playTimeInMilliseconds;
gameRepository.update(
{
objectID: game.objectId,
shop: "steam",
lastTimePlayed: updatedLastTimePlayed,
playTimeInMilliseconds: updatedPlayTime,
},
{ remoteId: game.id }
);
} else {
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
name: "getById",
});
if (steamGame) {
const iconUrl = steamGame?.clientIcon
? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
: null;
gameRepository.insert({
objectID: game.objectId,
title: steamGame?.name,
remoteId: game.id,
shop: game.shop,
iconUrl,
lastTimePlayed: game.lastTimePlayed,
playTimeInMilliseconds: game.playTimeInMilliseconds,
});
}
}
}
} catch (err) {
if (err instanceof AxiosError) {
logger.error("getRemoteGames", err.response, err.message);
} else {
logger.error("getRemoteGames", err);
}
}
};

View File

@ -0,0 +1,13 @@
import { Game } from "@main/entity";
import { HydraApi } from "../hydra-api";
export const updateGamePlaytime = async (
game: Game,
delta: number,
lastTimePlayed: Date
) => {
return HydraApi.put(`/games/${game.remoteId}`, {
playTimeDeltaInSeconds: Math.trunc(delta),
lastTimePlayed,
});
};

View File

@ -0,0 +1,44 @@
import { gameRepository } from "@main/repository";
import { chunk } from "lodash-es";
import { IsNull } from "typeorm";
import { HydraApi } from "../hydra-api";
import { logger } from "../logger";
import { AxiosError } from "axios";
import { mergeWithRemoteGames } from "./merge-with-remote-games";
import { WindowManager } from "../window-manager";
export const uploadGamesBatch = async () => {
try {
const games = await gameRepository.find({
where: { remoteId: IsNull(), isDeleted: false },
});
const gamesChunks = chunk(games, 200);
for (const chunk of gamesChunks) {
await HydraApi.post(
"/games/batch",
chunk.map((game) => {
return {
objectId: game.objectID,
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
shop: game.shop,
lastTimePlayed: game.lastTimePlayed,
};
})
);
}
await mergeWithRemoteGames();
if (WindowManager.mainWindow)
WindowManager.mainWindow.webContents.send("on-library-batch-complete");
} catch (err) {
if (err instanceof AxiosError) {
logger.error("uploadGamesBatch", err.response, err.message);
} else {
logger.error("uploadGamesBatch", err);
}
}
};

View File

@ -4,8 +4,12 @@ import { IsNull, Not } from "typeorm";
import { gameRepository } from "@main/repository";
import { getProcesses } from "@main/helpers";
import { WindowManager } from "./window-manager";
import { createGame, updateGamePlaytime } from "./library-sync";
const gamesPlaytime = new Map<number, number>();
const gamesPlaytime = new Map<
number,
{ lastTick: number; firstTick: number }
>();
export const watchProcesses = async () => {
const games = await gameRepository.find({
@ -37,18 +41,57 @@ export const watchProcesses = async () => {
if (gameProcess) {
if (gamesPlaytime.has(game.id)) {
const zero = gamesPlaytime.get(game.id) ?? 0;
const gamePlaytime = gamesPlaytime.get(game.id)!;
const zero = gamePlaytime.lastTick;
const delta = performance.now() - zero;
await gameRepository.update(game.id, {
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
lastTimePlayed: new Date(),
});
gamesPlaytime.set(game.id, {
...gamePlaytime,
lastTick: performance.now(),
});
} else {
if (game.remoteId) {
updateGamePlaytime(game, 0, new Date());
} else {
createGame({ ...game, lastTimePlayed: new Date() }).then(
(response) => {
const { id: remoteId } = response.data;
gameRepository.update({ objectID: game.objectID }, { remoteId });
}
);
}
gamesPlaytime.set(game.id, {
lastTick: performance.now(),
firstTick: performance.now(),
});
}
} else if (gamesPlaytime.has(game.id)) {
const gamePlaytime = gamesPlaytime.get(game.id)!;
gamesPlaytime.delete(game.id);
if (game.remoteId) {
updateGamePlaytime(
game,
performance.now() - gamePlaytime.firstTick,
game.lastTimePlayed!
);
} else {
createGame(game).then((response) => {
const { id: remoteId } = response.data;
gameRepository.update({ objectID: game.objectID }, { remoteId });
});
}
gamesPlaytime.set(game.id, performance.now());
} else if (gamesPlaytime.has(game.id)) {
gamesPlaytime.delete(game.id);
if (WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send("on-game-close", game.id);
}
}
}

View File

@ -90,6 +90,12 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.on("on-games-running", listener);
return () => ipcRenderer.removeListener("on-games-running", listener);
},
onLibraryBatchComplete: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb();
ipcRenderer.on("on-library-batch-complete", listener);
return () =>
ipcRenderer.removeListener("on-library-batch-complete", listener);
},
/* Hardware */
getDiskFreeSpace: (path: string) =>

View File

@ -127,6 +127,9 @@ export function App() {
if (response) updateUserDetails(response);
});
}),
window.electron.onLibraryBatchComplete(() => {
updateLibrary();
}),
window.electron.onSignOut(() => {
clearUserDetails();
}),

View File

@ -9,8 +9,8 @@ import {
} from "@renderer/helpers";
import { useTranslation } from "react-i18next";
const FEATURED_GAME_TITLE = "Horizon Forbidden West™ Complete Edition";
const FEATURED_GAME_ID = "2420110";
const FEATURED_GAME_TITLE = "Ghost of Tsushima DIRECTOR'S CUT";
const FEATURED_GAME_ID = "2215430";
export function Hero() {
const [featuredGameDetails, setFeaturedGameDetails] =

View File

@ -1,6 +1,6 @@
import { Downloader } from "@shared";
export const VERSION_CODENAME = "Exodus";
export const VERSION_CODENAME = "Leviticus";
export const DOWNLOADER_NAME = {
[Downloader.RealDebrid]: "Real-Debrid",

View File

@ -74,6 +74,7 @@ declare global {
onGamesRunning: (
cb: (gamesId: number[]) => void
) => () => Electron.IpcRenderer;
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
/* User preferences */
getUserPreferences: () => Promise<UserPreferences | null>;
@ -113,7 +114,7 @@ declare global {
checkForUpdates: () => Promise<boolean>;
restartAndInstallUpdate: () => Promise<void>;
/* Authg */
/* Auth */
signOut: () => Promise<void>;
onSignIn: (cb: () => void) => () => Electron.IpcRenderer;
onSignOut: (cb: () => void) => () => Electron.IpcRenderer;