diff --git a/.gitignore b/.gitignore index fb4badd7..7a6496a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .vscode node_modules hydra-download-manager/ +aria2/ fastlist.exe __pycache__ dist diff --git a/electron-builder.yml b/electron-builder.yml index fec128a6..be300d36 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -3,12 +3,10 @@ productName: Hydra directories: buildResources: build extraResources: + - aria2 - hydra-download-manager - seeds - - from: node_modules/ps-list/vendor/fastlist-0.3.0-x64.exe - to: fastlist.exe - from: node_modules/create-desktop-shortcuts/src/windows.vbs - - from: resources/hydralauncher.vbs files: - "!**/.vscode/*" - "!src/*" @@ -20,7 +18,6 @@ asarUnpack: - resources/** win: executableName: Hydra - requestedExecutionLevel: requireAdministrator target: - nsis - portable @@ -33,7 +30,6 @@ nsis: allowToChangeInstallationDirectory: true portable: artifactName: ${name}-${version}-portable.${ext} - requestExecutionLevel: admin mac: entitlementsInherit: build/entitlements.mac.plist extendInfo: diff --git a/package.json b/package.json index c029e4db..daaef390 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "start": "electron-vite preview", "dev": "electron-vite dev", "build": "npm run typecheck && electron-vite build", - "postinstall": "electron-builder install-app-deps", + "postinstall": "electron-builder install-app-deps && node ./postinstall.cjs", "build:unpack": "npm run build && electron-builder --dir", "build:win": "electron-vite build && electron-builder --win", "build:mac": "electron-vite build && electron-builder --mac", @@ -41,6 +41,7 @@ "@sentry/electron": "^5.1.0", "@vanilla-extract/css": "^1.14.2", "@vanilla-extract/recipes": "^0.5.2", + "aria2": "^4.1.2", "auto-launch": "^5.0.6", "axios": "^1.6.8", "better-sqlite3": "^9.5.0", @@ -65,11 +66,11 @@ "lottie-react": "^2.4.0", "parse-torrent": "^11.0.16", "piscina": "^4.5.1", - "ps-list": "^8.1.1", "react-i18next": "^14.1.0", "react-loading-skeleton": "^3.4.0", "react-redux": "^9.1.1", "react-router-dom": "^6.22.3", + "sudo-prompt": "^9.2.1", "typeorm": "^0.3.20", "user-agents": "^1.1.193", "yaml": "^2.4.1", diff --git a/postinstall.cjs b/postinstall.cjs new file mode 100644 index 00000000..547af988 --- /dev/null +++ b/postinstall.cjs @@ -0,0 +1,50 @@ +const { default: axios } = require("axios"); +const util = require("node:util"); +const fs = require("node:fs"); + +const exec = util.promisify(require("node:child_process").exec); + +const downloadAria2 = async () => { + if (fs.existsSync("aria2")) { + console.log("Aria2 already exists, skipping download..."); + return; + } + + const file = + process.platform === "win32" + ? "aria2-1.37.0-win-64bit-build1.zip" + : "aria2-1.37.0-1-x86_64.pkg.tar.zst"; + + const downloadUrl = + process.platform === "win32" + ? `https://github.com/aria2/aria2/releases/download/release-1.37.0/${file}` + : "https://archlinux.org/packages/extra/x86_64/aria2/download/"; + + console.log(`Downloading ${file}...`); + + const response = await axios.get(downloadUrl, { responseType: "stream" }); + + const stream = response.data.pipe(fs.createWriteStream(file)); + + stream.on("finish", async () => { + console.log(`Downloaded ${file}, extracting...`); + + if (process.platform === "win32") { + await exec(`npx extract-zip ${file}`); + console.log("Extracted. Renaming folder..."); + + fs.renameSync(file.replace(".zip", ""), "aria2"); + } else { + await exec(`tar --zstd -xvf ${file} usr/bin/aria2c`); + console.log("Extracted. Copying binary file..."); + fs.mkdirSync("aria2"); + fs.copyFileSync("usr/bin/aria2c", "aria2/aria2c"); + fs.rmSync("usr", { recursive: true }); + } + + console.log(`Extracted ${file}, removing compressed downloaded file...`); + fs.rmSync(file); + }); +}; + +downloadAria2(); diff --git a/requirements.txt b/requirements.txt index 6cee730a..b1488003 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ cx_Freeze cx_Logging; sys_platform == 'win32' lief; sys_platform == 'win32' pywin32; sys_platform == 'win32' +psutil diff --git a/resources/hydralauncher.vbs b/resources/hydralauncher.vbs deleted file mode 100644 index ff611acf..00000000 --- a/resources/hydralauncher.vbs +++ /dev/null @@ -1,3 +0,0 @@ -Set WshShell = CreateObject("WScript.Shell" ) -WshShell.Run """%localappdata%\Programs\Hydra\Hydra.exe""", 0 'Must quote command if it has spaces; must escape quotes -Set WshShell = Nothing diff --git a/src/main/constants.ts b/src/main/constants.ts index 62713a2c..850c9ada 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -14,12 +14,3 @@ export const logsPath = path.join(app.getPath("appData"), "hydra", "logs"); export const seedsPath = app.isPackaged ? path.join(process.resourcesPath, "seeds") : path.join(__dirname, "..", "..", "seeds"); - -export const windowsStartupPath = path.join( - app.getPath("appData"), - "Microsoft", - "Windows", - "Start Menu", - "Programs", - "Startup" -); diff --git a/src/main/declaration.d.ts b/src/main/declaration.d.ts new file mode 100644 index 00000000..ac2675a3 --- /dev/null +++ b/src/main/declaration.d.ts @@ -0,0 +1,80 @@ +declare module "aria2" { + export type Aria2Status = + | "active" + | "waiting" + | "paused" + | "error" + | "complete" + | "removed"; + + export interface StatusResponse { + gid: string; + status: Aria2Status; + totalLength: string; + completedLength: string; + uploadLength: string; + bitfield: string; + downloadSpeed: string; + uploadSpeed: string; + infoHash?: string; + numSeeders?: string; + seeder?: boolean; + pieceLength: string; + numPieces: string; + connections: string; + errorCode?: string; + errorMessage?: string; + followedBy?: string[]; + following: string; + belongsTo: string; + dir: string; + files: { + path: string; + length: string; + completedLength: string; + selected: string; + }[]; + bittorrent?: { + announceList: string[][]; + comment: string; + creationDate: string; + mode: "single" | "multi"; + info: { + name: string; + verifiedLength: string; + verifyIntegrityPending: string; + }; + }; + } + + export default class Aria2 { + constructor(options: any); + open: () => Promise; + call( + method: "addUri", + uris: string[], + options: { dir: string } + ): Promise; + call( + method: "tellStatus", + gid: string, + keys?: string[] + ): Promise; + call(method: "pause", gid: string): Promise; + call(method: "forcePause", gid: string): Promise; + call(method: "unpause", gid: string): Promise; + call(method: "remove", gid: string): Promise; + call(method: "forceRemove", gid: string): Promise; + call(method: "pauseAll"): Promise; + call(method: "forcePauseAll"): Promise; + listNotifications: () => [ + "onDownloadStart", + "onDownloadPause", + "onDownloadStop", + "onDownloadComplete", + "onDownloadError", + "onBtDownloadComplete", + ]; + on: (event: string, callback: (params: any) => void) => void; + } +} diff --git a/src/main/events/auth/sign-out.ts b/src/main/events/auth/sign-out.ts index 9c2ce2e0..fe640b9d 100644 --- a/src/main/events/auth/sign-out.ts +++ b/src/main/events/auth/sign-out.ts @@ -1,6 +1,6 @@ import { registerEvent } from "../register-event"; import * as Sentry from "@sentry/electron/main"; -import { HydraApi, TorrentDownloader, gamesPlaytime } from "@main/services"; +import { HydraApi, PythonInstance, gamesPlaytime } from "@main/services"; import { dataSource } from "@main/data-source"; import { DownloadQueue, Game, UserAuth } from "@main/entity"; @@ -24,11 +24,11 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => { Sentry.setUser(null); /* Disconnects libtorrent */ - TorrentDownloader.kill(); + PythonInstance.killTorrent(); await Promise.all([ databaseOperations, - HydraApi.post("/auth/logout").catch(), + HydraApi.post("/auth/logout").catch(() => {}), ]); }; diff --git a/src/main/events/index.ts b/src/main/events/index.ts index d1075e3e..1b500be9 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -22,7 +22,6 @@ import "./library/open-game-installer-path"; import "./library/update-executable-path"; import "./library/remove-game"; import "./library/remove-game-from-library"; -import "./misc/is-user-logged-in"; import "./misc/open-external"; import "./misc/show-open-dialog"; import "./torrenting/cancel-game-download"; diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 8187d41e..1bda0c93 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -53,18 +53,7 @@ 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 } - ); - }); + createGame(game!); }); }; diff --git a/src/main/events/library/close-game.ts b/src/main/events/library/close-game.ts index ccf446d3..9c431d06 100644 --- a/src/main/events/library/close-game.ts +++ b/src/main/events/library/close-game.ts @@ -1,39 +1,45 @@ -import path from "node:path"; - import { gameRepository } from "@main/repository"; -import { getProcesses } from "@main/helpers"; - import { registerEvent } from "../register-event"; +import { PythonInstance, logger } from "@main/services"; +import sudo from "sudo-prompt"; +import { app } from "electron"; + +const getKillCommand = (pid: number) => { + if (process.platform == "win32") { + return `taskkill /PID ${pid}`; + } + + return `kill -9 ${pid}`; +}; const closeGame = async ( _event: Electron.IpcMainInvokeEvent, gameId: number ) => { - const processes = await getProcesses(); + const processes = await PythonInstance.getProcessList(); const game = await gameRepository.findOne({ where: { id: gameId, isDeleted: false }, }); - if (!game) return false; - - const executablePath = game.executablePath!; - - const basename = path.win32.basename(executablePath); - const basenameWithoutExtension = path.win32.basename( - executablePath, - path.extname(executablePath) - ); + if (!game) return; const gameProcess = processes.find((runningProcess) => { - if (process.platform === "win32") { - return runningProcess.name === basename; - } - - return [basename, basenameWithoutExtension].includes(runningProcess.name); + return runningProcess.exe === game.executablePath; }); - if (gameProcess) return process.kill(gameProcess.pid); - return false; + if (gameProcess) { + try { + process.kill(gameProcess.pid); + } catch (err) { + sudo.exec( + getKillCommand(gameProcess.pid), + { name: app.getName() }, + (error, _stdout, _stderr) => { + logger.error(error); + } + ); + } + } }; registerEvent("closeGame", closeGame); diff --git a/src/main/events/library/create-game-shortcut.ts b/src/main/events/library/create-game-shortcut.ts index 30dfbf7c..a27317f0 100644 --- a/src/main/events/library/create-game-shortcut.ts +++ b/src/main/events/library/create-game-shortcut.ts @@ -4,6 +4,7 @@ import { IsNull, Not } from "typeorm"; import createDesktopShortcut from "create-desktop-shortcuts"; import path from "node:path"; import { app } from "electron"; +import { removeSymbolsFromName } from "@shared"; const createGameShortcut = async ( _event: Electron.IpcMainInvokeEvent, @@ -22,7 +23,7 @@ const createGameShortcut = async ( const options = { filePath, - name: game.title, + name: removeSymbolsFromName(game.title), }; return createDesktopShortcut({ diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index d7874b8f..468f5b26 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -20,7 +20,7 @@ const removeRemoveGameFromLibrary = async (gameId: number) => { const game = await gameRepository.findOne({ where: { id: gameId } }); if (game?.remoteId) { - HydraApi.delete(`/games/${game.remoteId}`); + HydraApi.delete(`/games/${game.remoteId}`).catch(() => {}); } }; diff --git a/src/main/events/misc/is-user-logged-in.ts b/src/main/events/misc/is-user-logged-in.ts deleted file mode 100644 index f5f79e15..00000000 --- a/src/main/events/misc/is-user-logged-in.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { registerEvent } from "../register-event"; -import { HydraApi } from "@main/services"; - -const isUserLoggedIn = async (_event: Electron.IpcMainInvokeEvent) => { - return HydraApi.isLoggedIn(); -}; - -registerEvent("isUserLoggedIn", isUserLoggedIn); diff --git a/src/main/events/profile/get-me.ts b/src/main/events/profile/get-me.ts index 17eb3d38..83463680 100644 --- a/src/main/events/profile/get-me.ts +++ b/src/main/events/profile/get-me.ts @@ -3,7 +3,7 @@ import * as Sentry from "@sentry/electron/main"; import { HydraApi } from "@main/services"; import { UserProfile } from "@types"; import { userAuthRepository } from "@main/repository"; -import { logger } from "@main/services"; +import { UserNotLoggedInError } from "@shared"; const getMe = async ( _event: Electron.IpcMainInvokeEvent @@ -27,7 +27,10 @@ const getMe = async ( return me; }) .catch((err) => { - logger.error("getMe", err.message); + if (err instanceof UserNotLoggedInError) { + return null; + } + return userAuthRepository.findOne({ where: { id: 1 } }); }); }; diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts index fbed0606..fe79d345 100644 --- a/src/main/events/profile/update-profile.ts +++ b/src/main/events/profile/update-profile.ts @@ -26,9 +26,11 @@ const updateProfile = async ( _event: Electron.IpcMainInvokeEvent, displayName: string, newProfileImagePath: string | null -): Promise => { +) => { if (!newProfileImagePath) { - return (await patchUserProfile(displayName)).data; + return patchUserProfile(displayName).then( + (response) => response.data as UserProfile + ); } const stats = fs.statSync(newProfileImagePath); @@ -51,11 +53,11 @@ const updateProfile = async ( }); return profileImageUrl; }) - .catch(() => { - return undefined; - }); + .catch(() => undefined); - return (await patchUserProfile(displayName, profileImageUrl)).data; + return patchUserProfile(displayName, profileImageUrl).then( + (response) => response.data as UserProfile + ); }; registerEvent("updateProfile", updateProfile); diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 0aa01fc9..cea41596 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -95,18 +95,7 @@ const startGameDownload = async ( }, }); - createGame(updatedGame!).then((response) => { - const { - id: remoteId, - playTimeInMilliseconds, - lastTimePlayed, - } = response.data; - - gameRepository.update( - { objectID }, - { remoteId, playTimeInMilliseconds, lastTimePlayed } - ); - }); + createGame(updatedGame!); await downloadQueueRepository.delete({ game: { id: updatedGame!.id } }); await downloadQueueRepository.insert({ game: { id: updatedGame!.id } }); diff --git a/src/main/events/user-preferences/auto-launch.ts b/src/main/events/user-preferences/auto-launch.ts index cb40a969..712f388b 100644 --- a/src/main/events/user-preferences/auto-launch.ts +++ b/src/main/events/user-preferences/auto-launch.ts @@ -1,9 +1,6 @@ -import { windowsStartupPath } from "@main/constants"; import { registerEvent } from "../register-event"; import AutoLaunch from "auto-launch"; import { app } from "electron"; -import fs from "node:fs"; -import path from "node:path"; const autoLaunch = async ( _event: Electron.IpcMainInvokeEvent, @@ -15,23 +12,10 @@ const autoLaunch = async ( name: app.getName(), }); - if (process.platform == "win32") { - const destination = path.join(windowsStartupPath, "Hydra.vbs"); - - if (enabled) { - const scriptPath = path.join(process.resourcesPath, "hydralauncher.vbs"); - - fs.copyFileSync(scriptPath, destination); - } else { - appLauncher.disable().catch(); - fs.rmSync(destination); - } + if (enabled) { + appLauncher.enable().catch(() => {}); } else { - if (enabled) { - appLauncher.enable().catch(); - } else { - appLauncher.disable().catch(); - } + appLauncher.disable().catch(() => {}); } }; diff --git a/src/main/events/user-preferences/update-user-preferences.ts b/src/main/events/user-preferences/update-user-preferences.ts index 66d56d4a..f45af519 100644 --- a/src/main/events/user-preferences/update-user-preferences.ts +++ b/src/main/events/user-preferences/update-user-preferences.ts @@ -2,17 +2,23 @@ import { userPreferencesRepository } from "@main/repository"; import { registerEvent } from "../register-event"; import type { UserPreferences } from "@types"; +import i18next from "i18next"; const updateUserPreferences = async ( _event: Electron.IpcMainInvokeEvent, preferences: Partial -) => - userPreferencesRepository.upsert( +) => { + if (preferences.language) { + i18next.changeLanguage(preferences.language); + } + + return userPreferencesRepository.upsert( { id: 1, ...preferences, }, ["id"] ); +}; registerEvent("updateUserPreferences", updateUserPreferences); diff --git a/src/main/helpers/index.ts b/src/main/helpers/index.ts index db1d13ff..902b927d 100644 --- a/src/main/helpers/index.ts +++ b/src/main/helpers/index.ts @@ -57,5 +57,4 @@ export const requestWebPage = async (url: string) => { .then((response) => response.data); }; -export * from "./ps"; export * from "./download-source"; diff --git a/src/main/helpers/ps.ts b/src/main/helpers/ps.ts deleted file mode 100644 index 5b326dfa..00000000 --- a/src/main/helpers/ps.ts +++ /dev/null @@ -1,41 +0,0 @@ -import psList from "ps-list"; -import path from "node:path"; -import childProcess from "node:child_process"; -import { promisify } from "node:util"; -import { app } from "electron"; - -const TEN_MEGABYTES = 1000 * 1000 * 10; -const execFile = promisify(childProcess.execFile); - -export const getProcesses = async () => { - if (process.platform == "win32") { - const binaryPath = app.isPackaged - ? path.join(process.resourcesPath, "fastlist.exe") - : path.join( - __dirname, - "..", - "..", - "node_modules", - "ps-list", - "vendor", - "fastlist-0.3.0-x64.exe" - ); - - const { stdout } = await execFile(binaryPath, { - maxBuffer: TEN_MEGABYTES, - windowsHide: true, - }); - - return stdout - .trim() - .split("\r\n") - .map((line) => line.split("\t")) - .map(([pid, ppid, name]) => ({ - pid: Number.parseInt(pid, 10), - ppid: Number.parseInt(ppid, 10), - name, - })); - } else { - return psList(); - } -}; diff --git a/src/main/index.ts b/src/main/index.ts index cb20733a..e288302b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,7 +5,7 @@ import i18n from "i18next"; import path from "node:path"; import url from "node:url"; import { electronApp, optimizer } from "@electron-toolkit/utils"; -import { logger, TorrentDownloader, WindowManager } from "@main/services"; +import { logger, PythonInstance, WindowManager } from "@main/services"; import { dataSource } from "@main/data-source"; import * as resources from "@locales"; import { userPreferencesRepository } from "@main/repository"; @@ -72,6 +72,10 @@ app.whenReady().then(async () => { where: { id: 1 }, }); + if (userPreferences?.language) { + i18n.changeLanguage(userPreferences.language); + } + WindowManager.createMainWindow(); WindowManager.createSystemTray(userPreferences?.language || "en"); }); @@ -116,7 +120,7 @@ app.on("window-all-closed", () => { app.on("before-quit", () => { /* Disconnects libtorrent */ - TorrentDownloader.kill(); + PythonInstance.kill(); }); app.on("activate", () => { diff --git a/src/main/main.ts b/src/main/main.ts index 1abd8c2f..ec95ccd5 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,4 +1,9 @@ -import { DownloadManager, RepacksManager, startMainLoop } from "./services"; +import { + DownloadManager, + RepacksManager, + PythonInstance, + startMainLoop, +} from "./services"; import { downloadQueueRepository, repackRepository, @@ -12,8 +17,6 @@ import { MoreThan } from "typeorm"; import { HydraApi } from "./services/hydra-api"; import { uploadGamesBatch } from "./services/library-sync"; -startMainLoop(); - const loadState = async (userPreferences: UserPreferences | null) => { await RepacksManager.updateRepacks(); @@ -22,8 +25,8 @@ const loadState = async (userPreferences: UserPreferences | null) => { if (userPreferences?.realDebridApiToken) RealDebridClient.authorize(userPreferences?.realDebridApiToken); - HydraApi.setupApi().then(async () => { - if (HydraApi.isLoggedIn()) uploadGamesBatch(); + HydraApi.setupApi().then(() => { + uploadGamesBatch(); }); const [nextQueueItem] = await downloadQueueRepository.find({ @@ -35,8 +38,13 @@ const loadState = async (userPreferences: UserPreferences | null) => { }, }); - if (nextQueueItem?.game.status === "active") + if (nextQueueItem?.game.status === "active") { DownloadManager.startDownload(nextQueueItem.game); + } else { + PythonInstance.spawn(); + } + + startMainLoop(); const now = new Date(); diff --git a/src/main/services/aria2c.ts b/src/main/services/aria2c.ts new file mode 100644 index 00000000..b1b1da76 --- /dev/null +++ b/src/main/services/aria2c.ts @@ -0,0 +1,20 @@ +import path from "node:path"; +import { spawn } from "node:child_process"; +import { app } from "electron"; + +export const startAria2 = () => { + const binaryPath = app.isPackaged + ? path.join(process.resourcesPath, "aria2", "aria2c") + : path.join(__dirname, "..", "..", "aria2", "aria2c"); + + return spawn( + binaryPath, + [ + "--enable-rpc", + "--rpc-listen-all", + "--file-allocation=none", + "--allow-overwrite=true", + ], + { stdio: "inherit", windowsHide: true } + ); +}; diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index f7553c9c..31f28992 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -1,6 +1,6 @@ import { Game } from "@main/entity"; import { Downloader } from "@shared"; -import { TorrentDownloader } from "./torrent-downloader"; +import { PythonInstance } from "./python-instance"; import { WindowManager } from "../window-manager"; import { downloadQueueRepository, gameRepository } from "@main/repository"; import { publishDownloadCompleteNotification } from "../notifications"; @@ -16,7 +16,7 @@ export class DownloadManager { if (this.currentDownloader === Downloader.RealDebrid) { status = await RealDebridDownloader.getStatus(); } else { - status = await TorrentDownloader.getStatus(); + status = await PythonInstance.getStatus(); } if (status) { @@ -63,9 +63,9 @@ export class DownloadManager { static async pauseDownload() { if (this.currentDownloader === Downloader.RealDebrid) { - RealDebridDownloader.pauseDownload(); + await RealDebridDownloader.pauseDownload(); } else { - await TorrentDownloader.pauseDownload(); + await PythonInstance.pauseDownload(); } WindowManager.mainWindow?.setProgressBar(-1); @@ -77,16 +77,16 @@ export class DownloadManager { RealDebridDownloader.startDownload(game); this.currentDownloader = Downloader.RealDebrid; } else { - TorrentDownloader.startDownload(game); + PythonInstance.startDownload(game); this.currentDownloader = Downloader.Torrent; } } static async cancelDownload(gameId: number) { if (this.currentDownloader === Downloader.RealDebrid) { - RealDebridDownloader.cancelDownload(); + RealDebridDownloader.cancelDownload(gameId); } else { - TorrentDownloader.cancelDownload(gameId); + PythonInstance.cancelDownload(gameId); } WindowManager.mainWindow?.setProgressBar(-1); @@ -98,7 +98,7 @@ export class DownloadManager { RealDebridDownloader.startDownload(game); this.currentDownloader = Downloader.RealDebrid; } else { - TorrentDownloader.startDownload(game); + PythonInstance.startDownload(game); this.currentDownloader = Downloader.Torrent; } } diff --git a/src/main/services/download/http-download.ts b/src/main/services/download/http-download.ts index 1ce99825..4553a6cb 100644 --- a/src/main/services/download/http-download.ts +++ b/src/main/services/download/http-download.ts @@ -1,123 +1,68 @@ -import path from "node:path"; -import fs from "node:fs"; -import crypto from "node:crypto"; - -import axios, { type AxiosProgressEvent } from "axios"; -import { app } from "electron"; +import type { ChildProcess } from "node:child_process"; import { logger } from "../logger"; +import { sleep } from "@main/helpers"; +import { startAria2 } from "../aria2c"; +import Aria2 from "aria2"; export class HttpDownload { - private abortController: AbortController; - public lastProgressEvent: AxiosProgressEvent; - private trackerFilePath: string; + private static connected = false; + private static aria2c: ChildProcess | null = null; - private trackerProgressEvent: AxiosProgressEvent | null = null; - private downloadPath: string; + private static aria2 = new Aria2({}); - private downloadTrackersPath = path.join( - app.getPath("documents"), - "Hydra", - "Downloads" - ); + private static async connect() { + this.aria2c = startAria2(); - constructor( - private url: string, - private savePath: string - ) { - this.abortController = new AbortController(); + let retries = 0; - const sha256Hasher = crypto.createHash("sha256"); - const hash = sha256Hasher.update(url).digest("hex"); + while (retries < 4 && !this.connected) { + try { + await this.aria2.open(); + logger.log("Connected to aria2"); - this.trackerFilePath = path.join( - this.downloadTrackersPath, - `${hash}.hydradownload` - ); - - const filename = path.win32.basename(this.url); - this.downloadPath = path.join(this.savePath, filename); - } - - private updateTrackerFile() { - if (!fs.existsSync(this.downloadTrackersPath)) { - fs.mkdirSync(this.downloadTrackersPath, { - recursive: true, - }); - } - - fs.writeFileSync( - this.trackerFilePath, - JSON.stringify(this.lastProgressEvent), - { encoding: "utf-8" } - ); - } - - private removeTrackerFile() { - if (fs.existsSync(this.trackerFilePath)) { - fs.rm(this.trackerFilePath, (err) => { - logger.error(err); - }); + this.connected = true; + } catch (err) { + await sleep(100); + logger.log("Failed to connect to aria2, retrying..."); + retries++; + } } } - public async startDownload() { - // Check if there's already a tracker file and download file - if ( - fs.existsSync(this.trackerFilePath) && - fs.existsSync(this.downloadPath) - ) { - this.trackerProgressEvent = JSON.parse( - fs.readFileSync(this.trackerFilePath, { encoding: "utf-8" }) - ); + public static getStatus(gid: string) { + if (this.connected) { + return this.aria2.call("tellStatus", gid); } - const response = await axios.get(this.url, { - responseType: "stream", - signal: this.abortController.signal, - headers: { - Range: `bytes=${this.trackerProgressEvent?.loaded ?? 0}-`, - }, - onDownloadProgress: (progressEvent) => { - const total = - this.trackerProgressEvent?.total ?? progressEvent.total ?? 0; - const loaded = - (this.trackerProgressEvent?.loaded ?? 0) + progressEvent.loaded; - - const progress = loaded / total; - - this.lastProgressEvent = { - ...progressEvent, - total, - progress, - loaded, - }; - this.updateTrackerFile(); - - if (progressEvent.progress === 1) { - this.removeTrackerFile(); - } - }, - }); - - response.data.pipe( - fs.createWriteStream(this.downloadPath, { - flags: "a", - }) - ); + return null; } - public async pauseDownload() { - this.abortController.abort(); + public static disconnect() { + if (this.aria2c) { + this.aria2c.kill(); + this.connected = false; + } } - public cancelDownload() { - this.pauseDownload(); + static async cancelDownload(gid: string) { + await this.aria2.call("forceRemove", gid); + } - fs.rm(this.downloadPath, (err) => { - if (err) logger.error(err); - }); - fs.rm(this.trackerFilePath, (err) => { - if (err) logger.error(err); - }); + static async pauseDownload(gid: string) { + await this.aria2.call("forcePause", gid); + } + + static async resumeDownload(gid: string) { + await this.aria2.call("unpause", gid); + } + + static async startDownload(downloadPath: string, downloadUrl: string) { + if (!this.connected) await this.connect(); + + const options = { + dir: downloadPath, + }; + + return this.aria2.call("addUri", [downloadUrl], options); } } diff --git a/src/main/services/download/index.ts b/src/main/services/download/index.ts index 6244b81a..26d21799 100644 --- a/src/main/services/download/index.ts +++ b/src/main/services/download/index.ts @@ -1,2 +1,2 @@ export * from "./download-manager"; -export * from "./torrent-downloader"; +export * from "./python-instance"; diff --git a/src/main/services/download/torrent-downloader.ts b/src/main/services/download/python-instance.ts similarity index 80% rename from src/main/services/download/torrent-downloader.ts rename to src/main/services/download/python-instance.ts index e35bb617..c534e41d 100644 --- a/src/main/services/download/torrent-downloader.ts +++ b/src/main/services/download/python-instance.ts @@ -1,7 +1,11 @@ import cp from "node:child_process"; import { Game } from "@main/entity"; -import { RPC_PASSWORD, RPC_PORT, startTorrentClient } from "./torrent-client"; +import { + RPC_PASSWORD, + RPC_PORT, + startTorrentClient as startRPCClient, +} from "./torrent-client"; import { gameRepository } from "@main/repository"; import { DownloadProgress } from "@types"; import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; @@ -13,10 +17,11 @@ import { PauseDownloadPayload, LibtorrentStatus, LibtorrentPayload, + ProcessPayload, } from "./types"; -export class TorrentDownloader { - private static torrentClient: cp.ChildProcess | null = null; +export class PythonInstance { + private static pythonProcess: cp.ChildProcess | null = null; private static downloadingGameId = -1; private static rpc = axios.create({ @@ -26,18 +31,31 @@ export class TorrentDownloader { }, }); - private static spawn(args: StartDownloadPayload) { - this.torrentClient = startTorrentClient(args); + public static spawn(args?: StartDownloadPayload) { + this.pythonProcess = startRPCClient(args); } public static kill() { - if (this.torrentClient) { - this.torrentClient.kill(); - this.torrentClient = null; + if (this.pythonProcess) { + this.pythonProcess.kill(); + this.pythonProcess = null; this.downloadingGameId = -1; } } + public static killTorrent() { + if (this.pythonProcess) { + this.rpc.post("/action", { action: "kill-torrent" }); + this.downloadingGameId = -1; + } + } + + public static async getProcessList() { + return ( + (await this.rpc.get("/process-list")).data || [] + ); + } + public static async getStatus() { if (this.downloadingGameId === -1) return null; @@ -113,7 +131,7 @@ export class TorrentDownloader { } static async startDownload(game: Game) { - if (!this.torrentClient) { + if (!this.pythonProcess) { this.spawn({ game_id: game.id, magnet: game.uri!, diff --git a/src/main/services/download/real-debrid-downloader.ts b/src/main/services/download/real-debrid-downloader.ts index 476d8f3e..8ead0067 100644 --- a/src/main/services/download/real-debrid-downloader.ts +++ b/src/main/services/download/real-debrid-downloader.ts @@ -6,10 +6,10 @@ import { DownloadProgress } from "@types"; import { HttpDownload } from "./http-download"; export class RealDebridDownloader { + private static downloads = new Map(); private static downloadingGame: Game | null = null; private static realDebridTorrentId: string | null = null; - private static httpDownload: HttpDownload | null = null; private static async getRealDebridDownloadUrl() { if (this.realDebridTorrentId) { @@ -35,39 +35,47 @@ export class RealDebridDownloader { } public static async getStatus() { - const lastProgressEvent = this.httpDownload?.lastProgressEvent; + if (this.downloadingGame) { + const gid = this.downloads.get(this.downloadingGame.id)!; + const status = await HttpDownload.getStatus(gid); - if (lastProgressEvent) { - await gameRepository.update( - { id: this.downloadingGame!.id }, - { - bytesDownloaded: lastProgressEvent.loaded, - fileSize: lastProgressEvent.total, - progress: lastProgressEvent.progress, - status: "active", + if (status) { + const progress = + Number(status.completedLength) / Number(status.totalLength); + + await gameRepository.update( + { id: this.downloadingGame!.id }, + { + bytesDownloaded: Number(status.completedLength), + fileSize: Number(status.totalLength), + progress, + status: "active", + } + ); + + const result = { + numPeers: 0, + numSeeds: 0, + downloadSpeed: Number(status.downloadSpeed), + timeRemaining: calculateETA( + Number(status.totalLength), + Number(status.completedLength), + Number(status.downloadSpeed) + ), + isDownloadingMetadata: false, + isCheckingFiles: false, + progress, + gameId: this.downloadingGame!.id, + } as DownloadProgress; + + if (progress === 1) { + this.downloads.delete(this.downloadingGame.id); + this.realDebridTorrentId = null; + this.downloadingGame = null; } - ); - const progress = { - numPeers: 0, - numSeeds: 0, - downloadSpeed: lastProgressEvent.rate, - timeRemaining: calculateETA( - lastProgressEvent.total ?? 0, - lastProgressEvent.loaded, - lastProgressEvent.rate ?? 0 - ), - isDownloadingMetadata: false, - isCheckingFiles: false, - progress: lastProgressEvent.progress, - gameId: this.downloadingGame!.id, - } as DownloadProgress; - - if (lastProgressEvent.progress === 1) { - this.pauseDownload(); + return result; } - - return progress; } if (this.realDebridTorrentId && this.downloadingGame) { @@ -101,25 +109,54 @@ export class RealDebridDownloader { } static async pauseDownload() { - this.httpDownload?.pauseDownload(); + const gid = this.downloads.get(this.downloadingGame!.id!); + if (gid) { + await HttpDownload.pauseDownload(gid); + } + this.realDebridTorrentId = null; this.downloadingGame = null; } static async startDownload(game: Game) { - this.realDebridTorrentId = await RealDebridClient.getTorrentId(game!.uri!); this.downloadingGame = game; + if (this.downloads.has(game.id)) { + await this.resumeDownload(game.id!); + + return; + } + + this.realDebridTorrentId = await RealDebridClient.getTorrentId(game!.uri!); + const downloadUrl = await this.getRealDebridDownloadUrl(); if (downloadUrl) { this.realDebridTorrentId = null; - this.httpDownload = new HttpDownload(downloadUrl, game!.downloadPath!); - this.httpDownload.startDownload(); + + const gid = await HttpDownload.startDownload( + game.downloadPath!, + downloadUrl + ); + + this.downloads.set(game.id!, gid); } } - static cancelDownload() { - return this.httpDownload?.cancelDownload(); + static async cancelDownload(gameId: number) { + const gid = this.downloads.get(gameId); + + if (gid) { + await HttpDownload.cancelDownload(gid); + this.downloads.delete(gameId); + } + } + + static async resumeDownload(gameId: number) { + const gid = this.downloads.get(gameId); + + if (gid) { + await HttpDownload.resumeDownload(gid); + } } } diff --git a/src/main/services/download/torrent-client.ts b/src/main/services/download/torrent-client.ts index 176ec664..93d20b7f 100644 --- a/src/main/services/download/torrent-client.ts +++ b/src/main/services/download/torrent-client.ts @@ -15,12 +15,12 @@ export const BITTORRENT_PORT = "5881"; export const RPC_PORT = "8084"; export const RPC_PASSWORD = crypto.randomBytes(32).toString("hex"); -export const startTorrentClient = (args: StartDownloadPayload) => { +export const startTorrentClient = (args?: StartDownloadPayload) => { const commonArgs = [ BITTORRENT_PORT, RPC_PORT, RPC_PASSWORD, - encodeURIComponent(JSON.stringify(args)), + args ? encodeURIComponent(JSON.stringify(args)) : "", ]; if (app.isPackaged) { diff --git a/src/main/services/download/types.ts b/src/main/services/download/types.ts index 516cd84f..fd8009a2 100644 --- a/src/main/services/download/types.ts +++ b/src/main/services/download/types.ts @@ -31,3 +31,8 @@ export interface LibtorrentPayload { status: LibtorrentStatus; gameId: number; } + +export interface ProcessPayload { + exe: string; + pid: number; +} diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index fb8b9a28..98b783f3 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -5,6 +5,7 @@ import url from "url"; import { uploadGamesBatch } from "./library-sync"; import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id"; import { logger } from "./logger"; +import { UserNotLoggedInError } from "@shared"; export class HydraApi { private static instance: AxiosInstance; @@ -19,7 +20,7 @@ export class HydraApi { expirationTimestamp: 0, }; - static isLoggedIn() { + private static isLoggedIn() { return this.userAuth.authToken !== ""; } @@ -127,14 +128,8 @@ export class HydraApi { } private static async revalidateAccessTokenIfExpired() { - if (!this.userAuth.authToken) { - userAuthRepository.delete({ id: 1 }); - logger.error("user is not logged in"); - this.sendSignOutEvent(); - throw new Error("user is not logged in"); - } - const now = new Date(); + if (this.userAuth.expirationTimestamp < now.getTime()) { try { const response = await this.instance.post(`/auth/refresh`, { @@ -190,6 +185,8 @@ export class HydraApi { }; static async get(url: string) { + if (!this.isLoggedIn()) throw new UserNotLoggedInError(); + await this.revalidateAccessTokenIfExpired(); return this.instance .get(url, this.getAxiosConfig()) @@ -197,6 +194,8 @@ export class HydraApi { } static async post(url: string, data?: any) { + if (!this.isLoggedIn()) throw new UserNotLoggedInError(); + await this.revalidateAccessTokenIfExpired(); return this.instance .post(url, data, this.getAxiosConfig()) @@ -204,6 +203,8 @@ export class HydraApi { } static async put(url: string, data?: any) { + if (!this.isLoggedIn()) throw new UserNotLoggedInError(); + await this.revalidateAccessTokenIfExpired(); return this.instance .put(url, data, this.getAxiosConfig()) @@ -211,6 +212,8 @@ export class HydraApi { } static async patch(url: string, data?: any) { + if (!this.isLoggedIn()) throw new UserNotLoggedInError(); + await this.revalidateAccessTokenIfExpired(); return this.instance .patch(url, data, this.getAxiosConfig()) @@ -218,6 +221,8 @@ export class HydraApi { } static async delete(url: string) { + if (!this.isLoggedIn()) throw new UserNotLoggedInError(); + await this.revalidateAccessTokenIfExpired(); return this.instance .delete(url, this.getAxiosConfig()) diff --git a/src/main/services/library-sync/create-game.ts b/src/main/services/library-sync/create-game.ts index 6e56a3de..c0e8b1f8 100644 --- a/src/main/services/library-sync/create-game.ts +++ b/src/main/services/library-sync/create-game.ts @@ -1,11 +1,25 @@ import { Game } from "@main/entity"; import { HydraApi } from "../hydra-api"; +import { gameRepository } from "@main/repository"; export const createGame = async (game: Game) => { - return HydraApi.post(`/games`, { + HydraApi.post(`/games`, { objectId: game.objectID, playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds), shop: game.shop, lastTimePlayed: game.lastTimePlayed, - }); + }) + .then((response) => { + const { + id: remoteId, + playTimeInMilliseconds, + lastTimePlayed, + } = response.data; + + gameRepository.update( + { objectID: game.objectID }, + { remoteId, playTimeInMilliseconds, lastTimePlayed } + ); + }) + .catch(() => {}); }; diff --git a/src/main/services/library-sync/merge-with-remote-games.ts b/src/main/services/library-sync/merge-with-remote-games.ts index 3a2db640..2162ea58 100644 --- a/src/main/services/library-sync/merge-with-remote-games.ts +++ b/src/main/services/library-sync/merge-with-remote-games.ts @@ -2,71 +2,63 @@ 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 || - (game.lastTimePlayed && - new Date(game.lastTimePlayed) > localGame.lastTimePlayed) - ? game.lastTimePlayed - : localGame.lastTimePlayed; - - const updatedPlayTime = - localGame.playTimeInMilliseconds < game.playTimeInMilliseconds - ? game.playTimeInMilliseconds - : localGame.playTimeInMilliseconds; - - gameRepository.update( - { + return HydraApi.get("/games") + .then(async (response) => { + for (const game of response.data) { + const localGame = await gameRepository.findOne({ + where: { objectID: game.objectId, - shop: "steam", }, - { - remoteId: game.id, - lastTimePlayed: updatedLastTimePlayed, - playTimeInMilliseconds: updatedPlayTime, - } - ); - } else { - const steamGame = await steamGamesWorker.run(Number(game.objectId), { - name: "getById", }); - if (steamGame) { - const iconUrl = steamGame?.clientIcon - ? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon) - : null; + if (localGame) { + const updatedLastTimePlayed = + localGame.lastTimePlayed == null || + (game.lastTimePlayed && + new Date(game.lastTimePlayed) > localGame.lastTimePlayed) + ? game.lastTimePlayed + : localGame.lastTimePlayed; - gameRepository.insert({ - objectID: game.objectId, - title: steamGame?.name, - remoteId: game.id, - shop: game.shop, - iconUrl, - lastTimePlayed: game.lastTimePlayed, - playTimeInMilliseconds: game.playTimeInMilliseconds, + const updatedPlayTime = + localGame.playTimeInMilliseconds < game.playTimeInMilliseconds + ? game.playTimeInMilliseconds + : localGame.playTimeInMilliseconds; + + gameRepository.update( + { + objectID: game.objectId, + shop: "steam", + }, + { + remoteId: game.id, + lastTimePlayed: updatedLastTimePlayed, + playTimeInMilliseconds: updatedPlayTime, + } + ); + } 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.message); - } else { - logger.error("getRemoteGames", err); - } - } + }) + .catch(() => {}); }; diff --git a/src/main/services/library-sync/update-game-playtime.ts b/src/main/services/library-sync/update-game-playtime.ts index 190d7e7d..39206a12 100644 --- a/src/main/services/library-sync/update-game-playtime.ts +++ b/src/main/services/library-sync/update-game-playtime.ts @@ -6,8 +6,8 @@ export const updateGamePlaytime = async ( deltaInMillis: number, lastTimePlayed: Date ) => { - return HydraApi.put(`/games/${game.remoteId}`, { + HydraApi.put(`/games/${game.remoteId}`, { playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000), lastTimePlayed, - }); + }).catch(() => {}); }; diff --git a/src/main/services/library-sync/upload-games-batch.ts b/src/main/services/library-sync/upload-games-batch.ts index 63eee82a..88f02375 100644 --- a/src/main/services/library-sync/upload-games-batch.ts +++ b/src/main/services/library-sync/upload-games-batch.ts @@ -2,43 +2,32 @@ 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 games = await gameRepository.find({ + where: { remoteId: IsNull(), isDeleted: false }, + }); - const gamesChunks = chunk(games, 200); + 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); - } + 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, + }; + }) + ).catch(() => {}); } + + await mergeWithRemoteGames(); + + if (WindowManager.mainWindow) + WindowManager.mainWindow.webContents.send("on-library-batch-complete"); }; diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index f20b4128..0f7efa62 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -1,11 +1,9 @@ -import path from "node:path"; - 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"; import { GameRunning } from "@types"; +import { PythonInstance } from "./download"; export const gamesPlaytime = new Map< number, @@ -21,23 +19,13 @@ export const watchProcesses = async () => { }); if (games.length === 0) return; - - const processes = await getProcesses(); + const processes = await PythonInstance.getProcessList(); for (const game of games) { const executablePath = game.executablePath!; - const basename = path.win32.basename(executablePath); - const basenameWithoutExtension = path.win32.basename( - executablePath, - path.extname(executablePath) - ); const gameProcess = processes.find((runningProcess) => { - if (process.platform === "win32") { - return runningProcess.name === basename; - } - - return [basename, basenameWithoutExtension].includes(runningProcess.name); + return executablePath == runningProcess.exe; }); if (gameProcess) { @@ -60,12 +48,7 @@ export const watchProcesses = async () => { 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 }); - } - ); + createGame({ ...game, lastTimePlayed: new Date() }); } gamesPlaytime.set(game.id, { @@ -84,10 +67,7 @@ export const watchProcesses = async () => { game.lastTimePlayed! ); } else { - createGame(game).then((response) => { - const { id: remoteId } = response.data; - gameRepository.update({ objectID: game.objectID }, { remoteId }); - }); + createGame(game); } } } diff --git a/src/preload/index.ts b/src/preload/index.ts index ecabe9e8..0cadbc03 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -112,7 +112,6 @@ contextBridge.exposeInMainWorld("electron", { getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"), isPortableVersion: () => ipcRenderer.invoke("isPortableVersion"), openExternal: (src: string) => ipcRenderer.invoke("openExternal", src), - isUserLoggedIn: () => ipcRenderer.invoke("isUserLoggedIn"), showOpenDialog: (options: Electron.OpenDialogOptions) => ipcRenderer.invoke("showOpenDialog", options), platform: process.platform, diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 0f9d3972..afce9622 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -93,12 +93,8 @@ export function App() { dispatch(setProfileBackground(profileBackground)); } - window.electron.isUserLoggedIn().then((isLoggedIn) => { - if (isLoggedIn) { - fetchUserDetails().then((response) => { - if (response) updateUserDetails(response); - }); - } + fetchUserDetails().then((response) => { + if (response) updateUserDetails(response); }); }, [fetchUserDetails, updateUserDetails, dispatch]); diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index ae89bb8b..48fa7aae 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -100,7 +100,6 @@ declare global { /* Misc */ openExternal: (src: string) => Promise; - isUserLoggedIn: () => Promise; getVersion: () => Promise; ping: () => string; getDefaultDownloadsPath: () => Promise; diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index 1d8257f4..e87f8ff6 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -57,8 +57,14 @@ export function useUserDetails() { ); const fetchUserDetails = useCallback(async () => { - return window.electron.getMe(); - }, []); + return window.electron.getMe().then((userDetails) => { + if (userDetails == null) { + clearUserDetails(); + } + + return userDetails; + }); + }, [clearUserDetails]); const patchUser = useCallback( async (displayName: string, imageProfileUrl: string | null) => { diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx index 4741668e..795aa8af 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx @@ -122,7 +122,7 @@ export function HeroPanelActions() {