From ce13f6aa218c154a65d14c253365872d17031987 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sat, 15 Jun 2024 02:15:58 -0300 Subject: [PATCH 01/15] feat: sync library --- src/main/entity/game.entity.ts | 3 + .../events/library/add-game-to-library.ts | 21 ++++++ .../library/remove-game-from-library.ts | 7 ++ src/main/main.ts | 75 ++++++++++++++++++- src/main/services/hydra-api.ts | 9 +++ src/main/services/process-watcher.ts | 59 ++++++++++++++- 6 files changed, 167 insertions(+), 7 deletions(-) diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 49ad2716..83cc4001 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -22,6 +22,9 @@ export class Game { @Column("text", { unique: true }) objectID: string; + @Column("text", { unique: true, nullable: true }) + remoteId: string; + @Column("text") title: string; diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 1c7447e5..7f283636 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -6,6 +6,7 @@ import type { GameShop } from "@types"; import { getFileBase64, getSteamAppAsset } from "@main/helpers"; import { steamGamesWorker } from "@main/workers"; +import { HydraApi } from "@main/services/hydra-api"; const addGameToLibrary = async ( _event: Electron.IpcMainInvokeEvent, @@ -49,6 +50,26 @@ const addGameToLibrary = async ( } }); } + + const game = await gameRepository.findOne({ where: { objectID } }); + + HydraApi.post("/games", { + objectId: objectID, + playTimeInMilliseconds: game?.playTimeInMilliseconds, + shop, + lastTimePlayed: game?.lastTimePlayed, + }).then((response) => { + const { + id: remoteId, + playTimeInMilliseconds, + lastTimePlayed, + } = response.data; + + gameRepository.update( + { objectID }, + { remoteId, playTimeInMilliseconds, lastTimePlayed } + ); + }); }); }; diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 29a7a635..8bbd83e3 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -1,5 +1,6 @@ import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; +import { HydraApi } from "@main/services/hydra-api"; const removeGameFromLibrary = async ( _event: Electron.IpcMainInvokeEvent, @@ -9,6 +10,12 @@ const removeGameFromLibrary = async ( { id: gameId }, { isDeleted: true, executablePath: null } ); + + const game = await gameRepository.findOne({ where: { id: gameId } }); + + if (game?.remoteId) { + HydraApi.delete(`/games/${game.remoteId}`); + } }; registerEvent("removeGameFromLibrary", removeGameFromLibrary); diff --git a/src/main/main.ts b/src/main/main.ts index 7e5692d5..a866972a 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,15 +1,22 @@ -import { DownloadManager, RepacksManager, startMainLoop } from "./services"; +import { + DownloadManager, + RepacksManager, + logger, + startMainLoop, +} from "./services"; import { downloadQueueRepository, + gameRepository, repackRepository, userPreferencesRepository, } from "./repository"; import { UserPreferences } from "./entity"; import { RealDebridClient } from "./services/real-debrid"; -import { fetchDownloadSourcesAndUpdate } from "./helpers"; +import { fetchDownloadSourcesAndUpdate, getSteamAppAsset } from "./helpers"; import { publishNewRepacksNotifications } from "./services/notifications"; import { MoreThan } from "typeorm"; import { HydraApi } from "./services/hydra-api"; +import { steamGamesWorker } from "./workers"; startMainLoop(); @@ -21,7 +28,69 @@ const loadState = async (userPreferences: UserPreferences | null) => { if (userPreferences?.realDebridApiToken) RealDebridClient.authorize(userPreferences?.realDebridApiToken); - HydraApi.setupApi(); + HydraApi.setupApi() + .then(async () => { + if (HydraApi.isLoggedIn()) { + 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) => { + logger.error("erro api GET: /games", err); + }); const [nextQueueItem] = await downloadQueueRepository.find({ order: { diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 6c275df0..a4463723 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -14,6 +14,10 @@ export class HydraApi { expirationTimestamp: 0, }; + static isLoggedIn() { + return this.userAuth.authToken !== ""; + } + static async handleExternalAuth(auth: string) { const { payload } = url.parse(auth, true).query; @@ -140,4 +144,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()); + } } diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index ea1b6355..577a7770 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -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 { HydraApi } from "./hydra-api"; -const gamesPlaytime = new Map(); +const gamesPlaytime = new Map< + number, + { lastTick: number; firstTick: number } +>(); export const watchProcesses = async () => { const games = await gameRepository.find({ @@ -37,7 +41,9 @@ 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; if (WindowManager.mainWindow) { @@ -48,12 +54,57 @@ export const watchProcesses = async () => { playTimeInMilliseconds: game.playTimeInMilliseconds + delta, lastTimePlayed: new Date(), }); - } - gamesPlaytime.set(game.id, performance.now()); + gamesPlaytime.set(game.id, { + ...gamePlaytime, + lastTick: performance.now(), + }); + } else { + if (game.remoteId) { + HydraApi.put(`/games/${game.remoteId}`, { + playTimeDeltaInMilliseconds: 0, + lastTimePlayed: new Date(), + }); + } else { + HydraApi.post("/games", { + objectId: game.objectID, + playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), + shop: game.shop, + 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) { + HydraApi.put(`/games/${game.remoteId}`, { + playTimeInMilliseconds: Math.round( + performance.now() - gamePlaytime.firstTick + ), + lastTimePlayed: game.lastTimePlayed, + }); + } else { + HydraApi.post("/games", { + objectId: game.objectID, + playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), + shop: game.shop, + lastTimePlayed: game.lastTimePlayed, + }).then((response) => { + const { id: remoteId } = response.data; + gameRepository.update({ objectID: game.objectID }, { remoteId }); + }); + } + if (WindowManager.mainWindow) { WindowManager.mainWindow.webContents.send("on-game-close", game.id); } From e14e49cdccc0e8a503dd9653d65c2a0de626cb2f Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 16 Jun 2024 23:43:18 -0300 Subject: [PATCH 02/15] feat: batch games and code refactor --- src/main/entity/game.entity.ts | 2 +- .../events/library/add-game-to-library.ts | 9 +-- src/main/main.ts | 80 ++----------------- src/main/services/hydra-api.ts | 4 + src/main/services/library-sync/create-game.ts | 11 +++ .../services/library-sync/get-remote-games.ts | 69 ++++++++++++++++ src/main/services/library-sync/index.ts | 4 + .../library-sync/update-game-playtime.ts | 13 +++ .../library-sync/upload-batch-games.ts | 35 ++++++++ src/main/services/process-watcher.ts | 40 ++++------ 10 files changed, 161 insertions(+), 106 deletions(-) create mode 100644 src/main/services/library-sync/create-game.ts create mode 100644 src/main/services/library-sync/get-remote-games.ts create mode 100644 src/main/services/library-sync/index.ts create mode 100644 src/main/services/library-sync/update-game-playtime.ts create mode 100644 src/main/services/library-sync/upload-batch-games.ts diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 83cc4001..b8607b73 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -23,7 +23,7 @@ export class Game { objectID: string; @Column("text", { unique: true, nullable: true }) - remoteId: string; + remoteId: string | null; @Column("text") title: string; diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 7f283636..8187d41e 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -6,7 +6,7 @@ import type { GameShop } from "@types"; import { getFileBase64, getSteamAppAsset } from "@main/helpers"; import { steamGamesWorker } from "@main/workers"; -import { HydraApi } from "@main/services/hydra-api"; +import { createGame } from "@main/services/library-sync"; const addGameToLibrary = async ( _event: Electron.IpcMainInvokeEvent, @@ -53,12 +53,7 @@ const addGameToLibrary = async ( const game = await gameRepository.findOne({ where: { objectID } }); - HydraApi.post("/games", { - objectId: objectID, - playTimeInMilliseconds: game?.playTimeInMilliseconds, - shop, - lastTimePlayed: game?.lastTimePlayed, - }).then((response) => { + createGame(game!).then((response) => { const { id: remoteId, playTimeInMilliseconds, diff --git a/src/main/main.ts b/src/main/main.ts index a866972a..f1a365fc 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,22 +1,16 @@ -import { - DownloadManager, - RepacksManager, - logger, - startMainLoop, -} from "./services"; +import { DownloadManager, RepacksManager, startMainLoop } from "./services"; import { downloadQueueRepository, - gameRepository, repackRepository, userPreferencesRepository, } from "./repository"; import { UserPreferences } from "./entity"; import { RealDebridClient } from "./services/real-debrid"; -import { fetchDownloadSourcesAndUpdate, getSteamAppAsset } from "./helpers"; +import { fetchDownloadSourcesAndUpdate } from "./helpers"; import { publishNewRepacksNotifications } from "./services/notifications"; import { MoreThan } from "typeorm"; import { HydraApi } from "./services/hydra-api"; -import { steamGamesWorker } from "./workers"; +import { getRemoteGames } from "./services/library-sync"; startMainLoop(); @@ -28,69 +22,11 @@ const loadState = async (userPreferences: UserPreferences | null) => { if (userPreferences?.realDebridApiToken) RealDebridClient.authorize(userPreferences?.realDebridApiToken); - HydraApi.setupApi() - .then(async () => { - if (HydraApi.isLoggedIn()) { - 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) => { - logger.error("erro api GET: /games", err); - }); + HydraApi.setupApi().then(async () => { + if (HydraApi.isLoggedIn()) { + getRemoteGames(); + } + }); const [nextQueueItem] = await downloadQueueRepository.find({ order: { diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index a4463723..7d2a1fdf 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -2,6 +2,7 @@ import { userAuthRepository } from "@main/repository"; import axios, { AxiosError, AxiosInstance } from "axios"; import { WindowManager } from "./window-manager"; import url from "url"; +import { getRemoteGames, uploadBatchGames } from "./library-sync"; export class HydraApi { private static instance: AxiosInstance; @@ -49,6 +50,9 @@ export class HydraApi { if (WindowManager.mainWindow) { WindowManager.mainWindow.webContents.send("on-signin"); + + await uploadBatchGames(); + await getRemoteGames(); } } diff --git a/src/main/services/library-sync/create-game.ts b/src/main/services/library-sync/create-game.ts new file mode 100644 index 00000000..823f56a6 --- /dev/null +++ b/src/main/services/library-sync/create-game.ts @@ -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.round(game.playTimeInMilliseconds), + shop: game.shop, + lastTimePlayed: game.lastTimePlayed, + }); +}; diff --git a/src/main/services/library-sync/get-remote-games.ts b/src/main/services/library-sync/get-remote-games.ts new file mode 100644 index 00000000..1a85ca2d --- /dev/null +++ b/src/main/services/library-sync/get-remote-games.ts @@ -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 getRemoteGames = 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); + } + } +}; diff --git a/src/main/services/library-sync/index.ts b/src/main/services/library-sync/index.ts new file mode 100644 index 00000000..aa0d94de --- /dev/null +++ b/src/main/services/library-sync/index.ts @@ -0,0 +1,4 @@ +export * from "./get-remote-games"; +export * from "./upload-batch-games"; +export * from "./update-game-playtime"; +export * from "./create-game"; diff --git a/src/main/services/library-sync/update-game-playtime.ts b/src/main/services/library-sync/update-game-playtime.ts new file mode 100644 index 00000000..271dc6a5 --- /dev/null +++ b/src/main/services/library-sync/update-game-playtime.ts @@ -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: delta, + lastTimePlayed, + }); +}; diff --git a/src/main/services/library-sync/upload-batch-games.ts b/src/main/services/library-sync/upload-batch-games.ts new file mode 100644 index 00000000..cfea9d39 --- /dev/null +++ b/src/main/services/library-sync/upload-batch-games.ts @@ -0,0 +1,35 @@ +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"; + +export const uploadBatchGames = 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.round(game.playTimeInMilliseconds), + shop: game.shop, + lastTimePlayed: game.lastTimePlayed, + }; + }) + ); + } + } catch (err) { + if (err instanceof AxiosError) { + logger.error("uploadBatchGames", err.response, err.message); + } else { + logger.error("uploadBatchGames", err); + } + } +}; diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 577a7770..de3af727 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -4,7 +4,7 @@ import { IsNull, Not } from "typeorm"; import { gameRepository } from "@main/repository"; import { getProcesses } from "@main/helpers"; import { WindowManager } from "./window-manager"; -import { HydraApi } from "./hydra-api"; +import { createGame, updateGamePlaytime } from "./library-sync"; const gamesPlaytime = new Map< number, @@ -61,20 +61,14 @@ export const watchProcesses = async () => { }); } else { if (game.remoteId) { - HydraApi.put(`/games/${game.remoteId}`, { - playTimeDeltaInMilliseconds: 0, - lastTimePlayed: new Date(), - }); + updateGamePlaytime(game, 0, new Date()); } else { - HydraApi.post("/games", { - objectId: game.objectID, - playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), - shop: game.shop, - lastTimePlayed: new Date(), - }).then((response) => { - const { id: remoteId } = response.data; - gameRepository.update({ objectID: game.objectID }, { remoteId }); - }); + createGame({ ...game, lastTimePlayed: new Date() }).then( + (response) => { + const { id: remoteId } = response.data; + gameRepository.update({ objectID: game.objectID }, { remoteId }); + } + ); } gamesPlaytime.set(game.id, { @@ -87,19 +81,13 @@ export const watchProcesses = async () => { gamesPlaytime.delete(game.id); if (game.remoteId) { - HydraApi.put(`/games/${game.remoteId}`, { - playTimeInMilliseconds: Math.round( - performance.now() - gamePlaytime.firstTick - ), - lastTimePlayed: game.lastTimePlayed, - }); + updateGamePlaytime( + game, + performance.now() - gamePlaytime.firstTick, + game.lastTimePlayed! + ); } else { - HydraApi.post("/games", { - objectId: game.objectID, - playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), - shop: game.shop, - lastTimePlayed: game.lastTimePlayed, - }).then((response) => { + createGame(game).then((response) => { const { id: remoteId } = response.data; gameRepository.update({ objectID: game.objectID }, { remoteId }); }); From da5cc11bff3ebeff1c1a3cad71c69ce9637ea2a4 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sat, 15 Jun 2024 02:15:58 -0300 Subject: [PATCH 03/15] feat: sync library --- src/main/entity/game.entity.ts | 3 + .../events/library/add-game-to-library.ts | 21 ++++++ .../library/remove-game-from-library.ts | 7 ++ src/main/main.ts | 75 ++++++++++++++++++- src/main/services/hydra-api.ts | 9 +++ src/main/services/process-watcher.ts | 59 ++++++++++++++- 6 files changed, 167 insertions(+), 7 deletions(-) diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 49ad2716..83cc4001 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -22,6 +22,9 @@ export class Game { @Column("text", { unique: true }) objectID: string; + @Column("text", { unique: true, nullable: true }) + remoteId: string; + @Column("text") title: string; diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 1c7447e5..7f283636 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -6,6 +6,7 @@ import type { GameShop } from "@types"; import { getFileBase64, getSteamAppAsset } from "@main/helpers"; import { steamGamesWorker } from "@main/workers"; +import { HydraApi } from "@main/services/hydra-api"; const addGameToLibrary = async ( _event: Electron.IpcMainInvokeEvent, @@ -49,6 +50,26 @@ const addGameToLibrary = async ( } }); } + + const game = await gameRepository.findOne({ where: { objectID } }); + + HydraApi.post("/games", { + objectId: objectID, + playTimeInMilliseconds: game?.playTimeInMilliseconds, + shop, + lastTimePlayed: game?.lastTimePlayed, + }).then((response) => { + const { + id: remoteId, + playTimeInMilliseconds, + lastTimePlayed, + } = response.data; + + gameRepository.update( + { objectID }, + { remoteId, playTimeInMilliseconds, lastTimePlayed } + ); + }); }); }; diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 29a7a635..8bbd83e3 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -1,5 +1,6 @@ import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; +import { HydraApi } from "@main/services/hydra-api"; const removeGameFromLibrary = async ( _event: Electron.IpcMainInvokeEvent, @@ -9,6 +10,12 @@ const removeGameFromLibrary = async ( { id: gameId }, { isDeleted: true, executablePath: null } ); + + const game = await gameRepository.findOne({ where: { id: gameId } }); + + if (game?.remoteId) { + HydraApi.delete(`/games/${game.remoteId}`); + } }; registerEvent("removeGameFromLibrary", removeGameFromLibrary); diff --git a/src/main/main.ts b/src/main/main.ts index 7e5692d5..a866972a 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,15 +1,22 @@ -import { DownloadManager, RepacksManager, startMainLoop } from "./services"; +import { + DownloadManager, + RepacksManager, + logger, + startMainLoop, +} from "./services"; import { downloadQueueRepository, + gameRepository, repackRepository, userPreferencesRepository, } from "./repository"; import { UserPreferences } from "./entity"; import { RealDebridClient } from "./services/real-debrid"; -import { fetchDownloadSourcesAndUpdate } from "./helpers"; +import { fetchDownloadSourcesAndUpdate, getSteamAppAsset } from "./helpers"; import { publishNewRepacksNotifications } from "./services/notifications"; import { MoreThan } from "typeorm"; import { HydraApi } from "./services/hydra-api"; +import { steamGamesWorker } from "./workers"; startMainLoop(); @@ -21,7 +28,69 @@ const loadState = async (userPreferences: UserPreferences | null) => { if (userPreferences?.realDebridApiToken) RealDebridClient.authorize(userPreferences?.realDebridApiToken); - HydraApi.setupApi(); + HydraApi.setupApi() + .then(async () => { + if (HydraApi.isLoggedIn()) { + 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) => { + logger.error("erro api GET: /games", err); + }); const [nextQueueItem] = await downloadQueueRepository.find({ order: { diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 6c275df0..a4463723 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -14,6 +14,10 @@ export class HydraApi { expirationTimestamp: 0, }; + static isLoggedIn() { + return this.userAuth.authToken !== ""; + } + static async handleExternalAuth(auth: string) { const { payload } = url.parse(auth, true).query; @@ -140,4 +144,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()); + } } diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index ea1b6355..577a7770 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -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 { HydraApi } from "./hydra-api"; -const gamesPlaytime = new Map(); +const gamesPlaytime = new Map< + number, + { lastTick: number; firstTick: number } +>(); export const watchProcesses = async () => { const games = await gameRepository.find({ @@ -37,7 +41,9 @@ 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; if (WindowManager.mainWindow) { @@ -48,12 +54,57 @@ export const watchProcesses = async () => { playTimeInMilliseconds: game.playTimeInMilliseconds + delta, lastTimePlayed: new Date(), }); - } - gamesPlaytime.set(game.id, performance.now()); + gamesPlaytime.set(game.id, { + ...gamePlaytime, + lastTick: performance.now(), + }); + } else { + if (game.remoteId) { + HydraApi.put(`/games/${game.remoteId}`, { + playTimeDeltaInMilliseconds: 0, + lastTimePlayed: new Date(), + }); + } else { + HydraApi.post("/games", { + objectId: game.objectID, + playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), + shop: game.shop, + 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) { + HydraApi.put(`/games/${game.remoteId}`, { + playTimeInMilliseconds: Math.round( + performance.now() - gamePlaytime.firstTick + ), + lastTimePlayed: game.lastTimePlayed, + }); + } else { + HydraApi.post("/games", { + objectId: game.objectID, + playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), + shop: game.shop, + lastTimePlayed: game.lastTimePlayed, + }).then((response) => { + const { id: remoteId } = response.data; + gameRepository.update({ objectID: game.objectID }, { remoteId }); + }); + } + if (WindowManager.mainWindow) { WindowManager.mainWindow.webContents.send("on-game-close", game.id); } From 7fc376b47fe7be4f67ac22d2bbab64990a04a099 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 16 Jun 2024 23:43:18 -0300 Subject: [PATCH 04/15] feat: batch games and code refactor --- src/main/entity/game.entity.ts | 2 +- .../events/library/add-game-to-library.ts | 9 +-- src/main/main.ts | 80 ++----------------- src/main/services/hydra-api.ts | 4 + src/main/services/library-sync/create-game.ts | 11 +++ .../services/library-sync/get-remote-games.ts | 69 ++++++++++++++++ src/main/services/library-sync/index.ts | 4 + .../library-sync/update-game-playtime.ts | 13 +++ .../library-sync/upload-batch-games.ts | 35 ++++++++ src/main/services/process-watcher.ts | 40 ++++------ 10 files changed, 161 insertions(+), 106 deletions(-) create mode 100644 src/main/services/library-sync/create-game.ts create mode 100644 src/main/services/library-sync/get-remote-games.ts create mode 100644 src/main/services/library-sync/index.ts create mode 100644 src/main/services/library-sync/update-game-playtime.ts create mode 100644 src/main/services/library-sync/upload-batch-games.ts diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 83cc4001..b8607b73 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -23,7 +23,7 @@ export class Game { objectID: string; @Column("text", { unique: true, nullable: true }) - remoteId: string; + remoteId: string | null; @Column("text") title: string; diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 7f283636..8187d41e 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -6,7 +6,7 @@ import type { GameShop } from "@types"; import { getFileBase64, getSteamAppAsset } from "@main/helpers"; import { steamGamesWorker } from "@main/workers"; -import { HydraApi } from "@main/services/hydra-api"; +import { createGame } from "@main/services/library-sync"; const addGameToLibrary = async ( _event: Electron.IpcMainInvokeEvent, @@ -53,12 +53,7 @@ const addGameToLibrary = async ( const game = await gameRepository.findOne({ where: { objectID } }); - HydraApi.post("/games", { - objectId: objectID, - playTimeInMilliseconds: game?.playTimeInMilliseconds, - shop, - lastTimePlayed: game?.lastTimePlayed, - }).then((response) => { + createGame(game!).then((response) => { const { id: remoteId, playTimeInMilliseconds, diff --git a/src/main/main.ts b/src/main/main.ts index a866972a..f1a365fc 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,22 +1,16 @@ -import { - DownloadManager, - RepacksManager, - logger, - startMainLoop, -} from "./services"; +import { DownloadManager, RepacksManager, startMainLoop } from "./services"; import { downloadQueueRepository, - gameRepository, repackRepository, userPreferencesRepository, } from "./repository"; import { UserPreferences } from "./entity"; import { RealDebridClient } from "./services/real-debrid"; -import { fetchDownloadSourcesAndUpdate, getSteamAppAsset } from "./helpers"; +import { fetchDownloadSourcesAndUpdate } from "./helpers"; import { publishNewRepacksNotifications } from "./services/notifications"; import { MoreThan } from "typeorm"; import { HydraApi } from "./services/hydra-api"; -import { steamGamesWorker } from "./workers"; +import { getRemoteGames } from "./services/library-sync"; startMainLoop(); @@ -28,69 +22,11 @@ const loadState = async (userPreferences: UserPreferences | null) => { if (userPreferences?.realDebridApiToken) RealDebridClient.authorize(userPreferences?.realDebridApiToken); - HydraApi.setupApi() - .then(async () => { - if (HydraApi.isLoggedIn()) { - 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) => { - logger.error("erro api GET: /games", err); - }); + HydraApi.setupApi().then(async () => { + if (HydraApi.isLoggedIn()) { + getRemoteGames(); + } + }); const [nextQueueItem] = await downloadQueueRepository.find({ order: { diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index a4463723..7d2a1fdf 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -2,6 +2,7 @@ import { userAuthRepository } from "@main/repository"; import axios, { AxiosError, AxiosInstance } from "axios"; import { WindowManager } from "./window-manager"; import url from "url"; +import { getRemoteGames, uploadBatchGames } from "./library-sync"; export class HydraApi { private static instance: AxiosInstance; @@ -49,6 +50,9 @@ export class HydraApi { if (WindowManager.mainWindow) { WindowManager.mainWindow.webContents.send("on-signin"); + + await uploadBatchGames(); + await getRemoteGames(); } } diff --git a/src/main/services/library-sync/create-game.ts b/src/main/services/library-sync/create-game.ts new file mode 100644 index 00000000..823f56a6 --- /dev/null +++ b/src/main/services/library-sync/create-game.ts @@ -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.round(game.playTimeInMilliseconds), + shop: game.shop, + lastTimePlayed: game.lastTimePlayed, + }); +}; diff --git a/src/main/services/library-sync/get-remote-games.ts b/src/main/services/library-sync/get-remote-games.ts new file mode 100644 index 00000000..1a85ca2d --- /dev/null +++ b/src/main/services/library-sync/get-remote-games.ts @@ -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 getRemoteGames = 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); + } + } +}; diff --git a/src/main/services/library-sync/index.ts b/src/main/services/library-sync/index.ts new file mode 100644 index 00000000..aa0d94de --- /dev/null +++ b/src/main/services/library-sync/index.ts @@ -0,0 +1,4 @@ +export * from "./get-remote-games"; +export * from "./upload-batch-games"; +export * from "./update-game-playtime"; +export * from "./create-game"; diff --git a/src/main/services/library-sync/update-game-playtime.ts b/src/main/services/library-sync/update-game-playtime.ts new file mode 100644 index 00000000..271dc6a5 --- /dev/null +++ b/src/main/services/library-sync/update-game-playtime.ts @@ -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: delta, + lastTimePlayed, + }); +}; diff --git a/src/main/services/library-sync/upload-batch-games.ts b/src/main/services/library-sync/upload-batch-games.ts new file mode 100644 index 00000000..cfea9d39 --- /dev/null +++ b/src/main/services/library-sync/upload-batch-games.ts @@ -0,0 +1,35 @@ +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"; + +export const uploadBatchGames = 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.round(game.playTimeInMilliseconds), + shop: game.shop, + lastTimePlayed: game.lastTimePlayed, + }; + }) + ); + } + } catch (err) { + if (err instanceof AxiosError) { + logger.error("uploadBatchGames", err.response, err.message); + } else { + logger.error("uploadBatchGames", err); + } + } +}; diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 577a7770..de3af727 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -4,7 +4,7 @@ import { IsNull, Not } from "typeorm"; import { gameRepository } from "@main/repository"; import { getProcesses } from "@main/helpers"; import { WindowManager } from "./window-manager"; -import { HydraApi } from "./hydra-api"; +import { createGame, updateGamePlaytime } from "./library-sync"; const gamesPlaytime = new Map< number, @@ -61,20 +61,14 @@ export const watchProcesses = async () => { }); } else { if (game.remoteId) { - HydraApi.put(`/games/${game.remoteId}`, { - playTimeDeltaInMilliseconds: 0, - lastTimePlayed: new Date(), - }); + updateGamePlaytime(game, 0, new Date()); } else { - HydraApi.post("/games", { - objectId: game.objectID, - playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), - shop: game.shop, - lastTimePlayed: new Date(), - }).then((response) => { - const { id: remoteId } = response.data; - gameRepository.update({ objectID: game.objectID }, { remoteId }); - }); + createGame({ ...game, lastTimePlayed: new Date() }).then( + (response) => { + const { id: remoteId } = response.data; + gameRepository.update({ objectID: game.objectID }, { remoteId }); + } + ); } gamesPlaytime.set(game.id, { @@ -87,19 +81,13 @@ export const watchProcesses = async () => { gamesPlaytime.delete(game.id); if (game.remoteId) { - HydraApi.put(`/games/${game.remoteId}`, { - playTimeInMilliseconds: Math.round( - performance.now() - gamePlaytime.firstTick - ), - lastTimePlayed: game.lastTimePlayed, - }); + updateGamePlaytime( + game, + performance.now() - gamePlaytime.firstTick, + game.lastTimePlayed! + ); } else { - HydraApi.post("/games", { - objectId: game.objectID, - playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), - shop: game.shop, - lastTimePlayed: game.lastTimePlayed, - }).then((response) => { + createGame(game).then((response) => { const { id: remoteId } = response.data; gameRepository.update({ objectID: game.objectID }, { remoteId }); }); From c01ed86071e3ec1d3282421fe94c1b1b752ed6e8 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 17 Jun 2024 22:22:35 -0300 Subject: [PATCH 05/15] debug logs --- src/main/services/hydra-api.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 7d2a1fdf..52ccce92 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -61,6 +61,35 @@ export class HydraApi { baseURL: import.meta.env.MAIN_VITE_API_URL, }); + this.instance.interceptors.request.use( + (request) => { + console.log(" ---- REQUEST -----"); + console.log(request.method, request.baseURL, request.data); + return request; + }, + (error) => { + console.log("request error", error); + return Promise.reject(error); + } + ); + + this.instance.interceptors.response.use( + (response) => { + console.log(" ---- RESPONSE -----"); + console.log( + response.status, + response.config.method, + response.config.url, + response.data + ); + return response; + }, + (error) => { + console.log("response error", error); + return Promise.reject(error); + } + ); + const userAuth = await userAuthRepository.findOne({ where: { id: 1 }, }); From 7eb69f6e1622835ca77f21cad6f18843df51d8ce Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 17 Jun 2024 22:25:56 -0300 Subject: [PATCH 06/15] fix debug log --- src/main/services/hydra-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 52ccce92..7b50f7a9 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -64,7 +64,7 @@ export class HydraApi { this.instance.interceptors.request.use( (request) => { console.log(" ---- REQUEST -----"); - console.log(request.method, request.baseURL, request.data); + console.log(request.method, request.url, request.data); return request; }, (error) => { From cf84bf56b385c0e37a7f4536197b2ebb9f4112b6 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:46:20 -0300 Subject: [PATCH 07/15] missing parse to int in put /games/:gameId --- src/main/services/library-sync/create-game.ts | 2 +- src/main/services/library-sync/update-game-playtime.ts | 2 +- src/main/services/library-sync/upload-batch-games.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/services/library-sync/create-game.ts b/src/main/services/library-sync/create-game.ts index 823f56a6..6e56a3de 100644 --- a/src/main/services/library-sync/create-game.ts +++ b/src/main/services/library-sync/create-game.ts @@ -4,7 +4,7 @@ import { HydraApi } from "../hydra-api"; export const createGame = async (game: Game) => { return HydraApi.post(`/games`, { objectId: game.objectID, - playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), + playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds), shop: game.shop, lastTimePlayed: game.lastTimePlayed, }); diff --git a/src/main/services/library-sync/update-game-playtime.ts b/src/main/services/library-sync/update-game-playtime.ts index 271dc6a5..efedf47c 100644 --- a/src/main/services/library-sync/update-game-playtime.ts +++ b/src/main/services/library-sync/update-game-playtime.ts @@ -7,7 +7,7 @@ export const updateGamePlaytime = async ( lastTimePlayed: Date ) => { return HydraApi.put(`/games/${game.remoteId}`, { - playTimeDeltaInSeconds: delta, + playTimeDeltaInSeconds: Math.trunc(delta), lastTimePlayed, }); }; diff --git a/src/main/services/library-sync/upload-batch-games.ts b/src/main/services/library-sync/upload-batch-games.ts index cfea9d39..5f12522b 100644 --- a/src/main/services/library-sync/upload-batch-games.ts +++ b/src/main/services/library-sync/upload-batch-games.ts @@ -18,7 +18,7 @@ export const uploadBatchGames = async () => { chunk.map((game) => { return { objectId: game.objectID, - playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), + playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds), shop: game.shop, lastTimePlayed: game.lastTimePlayed, }; From 6179fb9cf66e9491ff7d96c4f31cee717209e700 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:49:41 -0300 Subject: [PATCH 08/15] call batch games on hydra start up --- src/main/main.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/main.ts b/src/main/main.ts index f1a365fc..6c82fe34 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -10,7 +10,7 @@ import { fetchDownloadSourcesAndUpdate } from "./helpers"; import { publishNewRepacksNotifications } from "./services/notifications"; import { MoreThan } from "typeorm"; import { HydraApi } from "./services/hydra-api"; -import { getRemoteGames } from "./services/library-sync"; +import { getRemoteGames, uploadBatchGames } from "./services/library-sync"; startMainLoop(); @@ -24,6 +24,7 @@ const loadState = async (userPreferences: UserPreferences | null) => { HydraApi.setupApi().then(async () => { if (HydraApi.isLoggedIn()) { + await uploadBatchGames(); getRemoteGames(); } }); From ab4cf23f97dd729a4ca62e1f4de474c7d02c431e Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:53:20 -0300 Subject: [PATCH 09/15] post to /games on startGameDownload --- src/main/events/torrenting/start-game-download.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index e2115349..0aa01fc9 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -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 } }); From b07451e91d40a1303481b19bb5c438e8f8bf81b1 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:58:11 -0300 Subject: [PATCH 10/15] refactor removeGameFromLibrary to handle error --- src/main/events/library/remove-game-from-library.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 8bbd83e3..5d154aae 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -1,6 +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, @@ -11,6 +12,12 @@ const removeGameFromLibrary = async ( { 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) { From 944f3891bf3ed007c68394c2acd216dd0c952df9 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 19 Jun 2024 02:35:57 +0100 Subject: [PATCH 11/15] feat: dispatching event when remote games are fetched --- src/main/main.ts | 7 ++----- src/main/services/hydra-api.ts | 6 ++---- src/main/services/library-sync/index.ts | 4 ++-- ...remote-games.ts => merge-with-remote-games.ts} | 2 +- ...pload-batch-games.ts => upload-games-batch.ts} | 15 ++++++++++++--- src/preload/index.ts | 6 ++++++ src/renderer/src/app.tsx | 5 ++++- src/renderer/src/declaration.d.ts | 3 ++- 8 files changed, 31 insertions(+), 17 deletions(-) rename src/main/services/library-sync/{get-remote-games.ts => merge-with-remote-games.ts} (97%) rename src/main/services/library-sync/{upload-batch-games.ts => upload-games-batch.ts} (66%) diff --git a/src/main/main.ts b/src/main/main.ts index 6c82fe34..1abd8c2f 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -10,7 +10,7 @@ import { fetchDownloadSourcesAndUpdate } from "./helpers"; import { publishNewRepacksNotifications } from "./services/notifications"; import { MoreThan } from "typeorm"; import { HydraApi } from "./services/hydra-api"; -import { getRemoteGames, uploadBatchGames } from "./services/library-sync"; +import { uploadGamesBatch } from "./services/library-sync"; startMainLoop(); @@ -23,10 +23,7 @@ const loadState = async (userPreferences: UserPreferences | null) => { RealDebridClient.authorize(userPreferences?.realDebridApiToken); HydraApi.setupApi().then(async () => { - if (HydraApi.isLoggedIn()) { - await uploadBatchGames(); - getRemoteGames(); - } + if (HydraApi.isLoggedIn()) uploadGamesBatch(); }); const [nextQueueItem] = await downloadQueueRepository.find({ diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 7b50f7a9..009d15ac 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -2,7 +2,7 @@ import { userAuthRepository } from "@main/repository"; import axios, { AxiosError, AxiosInstance } from "axios"; import { WindowManager } from "./window-manager"; import url from "url"; -import { getRemoteGames, uploadBatchGames } from "./library-sync"; +import { uploadGamesBatch } from "./library-sync"; export class HydraApi { private static instance: AxiosInstance; @@ -50,9 +50,7 @@ export class HydraApi { if (WindowManager.mainWindow) { WindowManager.mainWindow.webContents.send("on-signin"); - - await uploadBatchGames(); - await getRemoteGames(); + uploadGamesBatch(); } } diff --git a/src/main/services/library-sync/index.ts b/src/main/services/library-sync/index.ts index aa0d94de..24fafc03 100644 --- a/src/main/services/library-sync/index.ts +++ b/src/main/services/library-sync/index.ts @@ -1,4 +1,4 @@ -export * from "./get-remote-games"; -export * from "./upload-batch-games"; +export * from "./merge-with-remote-games"; +export * from "./upload-games-batch"; export * from "./update-game-playtime"; export * from "./create-game"; diff --git a/src/main/services/library-sync/get-remote-games.ts b/src/main/services/library-sync/merge-with-remote-games.ts similarity index 97% rename from src/main/services/library-sync/get-remote-games.ts rename to src/main/services/library-sync/merge-with-remote-games.ts index 1a85ca2d..e31310fc 100644 --- a/src/main/services/library-sync/get-remote-games.ts +++ b/src/main/services/library-sync/merge-with-remote-games.ts @@ -5,7 +5,7 @@ import { getSteamAppAsset } from "@main/helpers"; import { logger } from "../logger"; import { AxiosError } from "axios"; -export const getRemoteGames = async () => { +export const mergeWithRemoteGames = async () => { try { const games = await HydraApi.get("/games"); diff --git a/src/main/services/library-sync/upload-batch-games.ts b/src/main/services/library-sync/upload-games-batch.ts similarity index 66% rename from src/main/services/library-sync/upload-batch-games.ts rename to src/main/services/library-sync/upload-games-batch.ts index 5f12522b..63eee82a 100644 --- a/src/main/services/library-sync/upload-batch-games.ts +++ b/src/main/services/library-sync/upload-games-batch.ts @@ -5,13 +5,17 @@ import { HydraApi } from "../hydra-api"; import { logger } from "../logger"; import { AxiosError } from "axios"; -export const uploadBatchGames = async () => { +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", @@ -25,11 +29,16 @@ export const uploadBatchGames = async () => { }) ); } + + await mergeWithRemoteGames(); + + if (WindowManager.mainWindow) + WindowManager.mainWindow.webContents.send("on-library-batch-complete"); } catch (err) { if (err instanceof AxiosError) { - logger.error("uploadBatchGames", err.response, err.message); + logger.error("uploadGamesBatch", err.response, err.message); } else { - logger.error("uploadBatchGames", err); + logger.error("uploadGamesBatch", err); } } }; diff --git a/src/preload/index.ts b/src/preload/index.ts index faae6ef9..883091f6 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -96,6 +96,12 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.on("on-game-close", listener); return () => ipcRenderer.removeListener("on-game-close", 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) => diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 792a2df2..916105e7 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -81,6 +81,9 @@ export function App() { window.electron.onSignIn(() => { updateUser(); }), + window.electron.onLibraryBatchComplete(() => { + updateLibrary(); + }), window.electron.onSignOut(() => { clearUser(); }), @@ -89,7 +92,7 @@ export function App() { return () => { listeners.forEach((unsubscribe) => unsubscribe()); }; - }, [updateUser, clearUser]); + }, [updateUser, updateLibrary, clearUser]); const handleSearch = useCallback( (query: string) => { diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 48c7bdc6..dd73ffb5 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -73,6 +73,7 @@ declare global { getGameByObjectID: (objectID: string) => Promise; onPlaytime: (cb: (gameId: number) => void) => () => Electron.IpcRenderer; onGameClose: (cb: (gameId: number) => void) => () => Electron.IpcRenderer; + onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer; /* User preferences */ getUserPreferences: () => Promise; @@ -111,7 +112,7 @@ declare global { checkForUpdates: () => Promise; restartAndInstallUpdate: () => Promise; - /* Authg */ + /* Auth */ signOut: () => Promise; onSignIn: (cb: () => void) => () => Electron.IpcRenderer; onSignOut: (cb: () => void) => () => Electron.IpcRenderer; From 2c9129c7b6d1370198bc469f8b73f240d1b2f86e Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:15:16 -0300 Subject: [PATCH 12/15] delete user auth if it is not logged in --- src/main/services/hydra-api.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 009d15ac..5e8e74b8 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -100,7 +100,10 @@ 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 }); + throw new Error("user is not logged in"); + } const now = new Date(); if (this.userAuth.expirationTimestamp < now.getTime()) { From 2e5a324669249081ce0e2996699e74faa069f716 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:07:15 -0300 Subject: [PATCH 13/15] clear remote ids on sign in --- src/main/services/hydra-api.ts | 2 ++ src/main/services/library-sync/clear-games-remote-id.ts | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 src/main/services/library-sync/clear-games-remote-id.ts diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 5e8e74b8..b398cbd8 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -3,6 +3,7 @@ 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"; export class HydraApi { private static instance: AxiosInstance; @@ -50,6 +51,7 @@ export class HydraApi { if (WindowManager.mainWindow) { WindowManager.mainWindow.webContents.send("on-signin"); + await clearGamesRemoteIds(); uploadGamesBatch(); } } diff --git a/src/main/services/library-sync/clear-games-remote-id.ts b/src/main/services/library-sync/clear-games-remote-id.ts new file mode 100644 index 00000000..f26d65f1 --- /dev/null +++ b/src/main/services/library-sync/clear-games-remote-id.ts @@ -0,0 +1,5 @@ +import { gameRepository } from "@main/repository"; + +export const clearGamesRemoteIds = () => { + return gameRepository.update({}, { remoteId: null }); +}; From 3b2b78dc7c89aa59417c83a1fead15d4d7a21d8c Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:09:13 -0300 Subject: [PATCH 14/15] use logger --- src/main/services/hydra-api.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index b398cbd8..87bb4077 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -4,6 +4,7 @@ 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; @@ -63,20 +64,20 @@ export class HydraApi { this.instance.interceptors.request.use( (request) => { - console.log(" ---- REQUEST -----"); - console.log(request.method, request.url, 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 +86,7 @@ export class HydraApi { return response; }, (error) => { - console.log("response error", error); + logger.error("response error", error); return Promise.reject(error); } ); @@ -104,6 +105,7 @@ export class HydraApi { private static async revalidateAccessTokenIfExpired() { if (!this.userAuth.authToken) { userAuthRepository.delete({ id: 1 }); + logger.error("user is not logged in"); throw new Error("user is not logged in"); } @@ -146,6 +148,8 @@ export class HydraApi { if (WindowManager.mainWindow) { WindowManager.mainWindow.webContents.send("on-signout"); } + + logger.log("user refresh token expired"); } throw err; From 0805728a7923cf7b64182a7ebf2c7ca0a85db325 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 19 Jun 2024 23:24:50 +0100 Subject: [PATCH 15/15] ci: updating version and hero --- package.json | 2 +- src/renderer/src/components/hero/hero.tsx | 4 ++-- src/renderer/src/constants.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 31bac590..4f85d682 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydralauncher", - "version": "1.2.4", + "version": "2.0.0", "description": "Hydra", "main": "./out/main/index.js", "author": "Los Broxas", diff --git a/src/renderer/src/components/hero/hero.tsx b/src/renderer/src/components/hero/hero.tsx index 0cfdbea5..79d9b586 100644 --- a/src/renderer/src/components/hero/hero.tsx +++ b/src/renderer/src/components/hero/hero.tsx @@ -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] = diff --git a/src/renderer/src/constants.ts b/src/renderer/src/constants.ts index 97bb0b57..6186bb85 100644 --- a/src/renderer/src/constants.ts +++ b/src/renderer/src/constants.ts @@ -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",