mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-21 18:13:55 +00:00
feat: adding new messages to hero panel
This commit is contained in:
@@ -6,7 +6,7 @@ import { gameRepository, userPreferencesRepository } from "@main/repository";
|
||||
import path from "node:path";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { RealDebridClient } from "./real-debrid";
|
||||
import { Notification } from "electron";
|
||||
import { Notification, app } from "electron";
|
||||
import { t } from "i18next";
|
||||
import { Downloader } from "@shared";
|
||||
import { DownloadProgress } from "@types";
|
||||
@@ -16,33 +16,36 @@ import { Game } from "@main/entity";
|
||||
export class DownloadManager {
|
||||
private static downloads = new Map<number, string>();
|
||||
|
||||
private static connected = false;
|
||||
private static gid: string | null = null;
|
||||
private static gameId: number | null = null;
|
||||
|
||||
private static aria2 = new Aria2({});
|
||||
|
||||
static async connect() {
|
||||
const binary = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"aria2-1.37.0-win-64bit-build1",
|
||||
"aria2c"
|
||||
);
|
||||
private static connect(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const binaryPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "aria2", "aria2c")
|
||||
: path.join(__dirname, "..", "..", "aria2", "aria2c");
|
||||
|
||||
spawn(
|
||||
binary,
|
||||
[
|
||||
const cp = spawn(binaryPath, [
|
||||
"--enable-rpc",
|
||||
"--rpc-listen-all",
|
||||
"--file-allocation=none",
|
||||
"--allow-overwrite=true",
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
]);
|
||||
|
||||
await this.aria2.open();
|
||||
this.attachListener();
|
||||
cp.stdout.on("data", async (data) => {
|
||||
const msg = Buffer.from(data).toString("utf-8");
|
||||
|
||||
if (msg.includes("IPv6 RPC: listening on TCP")) {
|
||||
await this.aria2.open();
|
||||
this.connected = true;
|
||||
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static getETA(status: StatusResponse) {
|
||||
@@ -84,91 +87,78 @@ export class DownloadManager {
|
||||
return "";
|
||||
}
|
||||
|
||||
private static async attachListener() {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
if (!this.gid || !this.gameId) {
|
||||
continue;
|
||||
public static async watchDownloads() {
|
||||
if (!this.gid || !this.gameId) return;
|
||||
|
||||
const status = await this.aria2.call("tellStatus", this.gid);
|
||||
|
||||
const downloadingMetadata = status.bittorrent && !status.bittorrent?.info;
|
||||
|
||||
if (status.followedBy?.length) {
|
||||
this.gid = status.followedBy[0];
|
||||
this.downloads.set(this.gameId, this.gid);
|
||||
return;
|
||||
}
|
||||
|
||||
const progress =
|
||||
Number(status.completedLength) / Number(status.totalLength);
|
||||
|
||||
if (!downloadingMetadata) {
|
||||
const update: QueryDeepPartialEntity<Game> = {
|
||||
bytesDownloaded: Number(status.completedLength),
|
||||
fileSize: Number(status.totalLength),
|
||||
status: status.status,
|
||||
};
|
||||
|
||||
if (!isNaN(progress)) update.progress = progress;
|
||||
|
||||
await gameRepository.update(
|
||||
{ id: this.gameId },
|
||||
{
|
||||
...update,
|
||||
status: status.status,
|
||||
folderName: this.getFolderName(status),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const status = await this.aria2.call("tellStatus", this.gid);
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id: this.gameId, isDeleted: false },
|
||||
relations: { repack: true },
|
||||
});
|
||||
|
||||
const downloadingMetadata =
|
||||
status.bittorrent && !status.bittorrent?.info;
|
||||
|
||||
if (status.followedBy?.length) {
|
||||
this.gid = status.followedBy[0];
|
||||
this.downloads.set(this.gameId, this.gid);
|
||||
continue;
|
||||
}
|
||||
|
||||
const progress =
|
||||
Number(status.completedLength) / Number(status.totalLength);
|
||||
|
||||
if (!downloadingMetadata) {
|
||||
const update: QueryDeepPartialEntity<Game> = {
|
||||
bytesDownloaded: Number(status.completedLength),
|
||||
fileSize: Number(status.totalLength),
|
||||
status: status.status,
|
||||
};
|
||||
|
||||
if (!isNaN(progress)) update.progress = progress;
|
||||
|
||||
await gameRepository.update(
|
||||
{ id: this.gameId },
|
||||
{
|
||||
...update,
|
||||
status: status.status,
|
||||
folderName: this.getFolderName(status),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id: this.gameId, isDeleted: false },
|
||||
relations: { repack: true },
|
||||
});
|
||||
|
||||
if (progress === 1 && game && !downloadingMetadata) {
|
||||
await this.publishNotification();
|
||||
/*
|
||||
Only cancel bittorrent downloads to stop seeding
|
||||
*/
|
||||
if (status.bittorrent) {
|
||||
await this.cancelDownload(game.id);
|
||||
} else {
|
||||
this.clearCurrentDownload();
|
||||
}
|
||||
}
|
||||
|
||||
if (WindowManager.mainWindow && game) {
|
||||
WindowManager.mainWindow.setProgressBar(
|
||||
progress === 1 || downloadingMetadata ? -1 : progress,
|
||||
{ mode: downloadingMetadata ? "indeterminate" : "normal" }
|
||||
);
|
||||
|
||||
const payload = {
|
||||
progress,
|
||||
bytesDownloaded: Number(status.completedLength),
|
||||
fileSize: Number(status.totalLength),
|
||||
numPeers: Number(status.connections),
|
||||
numSeeds: Number(status.numSeeders ?? 0),
|
||||
downloadSpeed: Number(status.downloadSpeed),
|
||||
timeRemaining: this.getETA(status),
|
||||
downloadingMetadata: !!downloadingMetadata,
|
||||
game,
|
||||
} as DownloadProgress;
|
||||
|
||||
WindowManager.mainWindow.webContents.send(
|
||||
"on-download-progress",
|
||||
JSON.parse(JSON.stringify(payload))
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
if (progress === 1 && game && !downloadingMetadata) {
|
||||
await this.publishNotification();
|
||||
/*
|
||||
Only cancel bittorrent downloads to stop seeding
|
||||
*/
|
||||
if (status.bittorrent) {
|
||||
await this.cancelDownload(game.id);
|
||||
} else {
|
||||
this.clearCurrentDownload();
|
||||
}
|
||||
}
|
||||
|
||||
if (WindowManager.mainWindow && game) {
|
||||
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
||||
|
||||
const payload = {
|
||||
progress,
|
||||
bytesDownloaded: Number(status.completedLength),
|
||||
fileSize: Number(status.totalLength),
|
||||
numPeers: Number(status.connections),
|
||||
numSeeds: Number(status.numSeeders ?? 0),
|
||||
downloadSpeed: Number(status.downloadSpeed),
|
||||
timeRemaining: this.getETA(status),
|
||||
downloadingMetadata: !!downloadingMetadata,
|
||||
game,
|
||||
} as DownloadProgress;
|
||||
|
||||
WindowManager.mainWindow.webContents.send(
|
||||
"on-download-progress",
|
||||
JSON.parse(JSON.stringify(payload))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static async getGame(gameId: number) {
|
||||
@@ -227,6 +217,8 @@ export class DownloadManager {
|
||||
}
|
||||
|
||||
static async startDownload(gameId: number) {
|
||||
if (!this.connected) await this.connect();
|
||||
|
||||
const game = await this.getGame(gameId)!;
|
||||
|
||||
if (game) {
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from "./window-manager";
|
||||
export * from "./download-manager";
|
||||
export * from "./how-long-to-beat";
|
||||
export * from "./process-watcher";
|
||||
export * from "./main-loop";
|
||||
|
||||
16
src/main/services/main-loop.ts
Normal file
16
src/main/services/main-loop.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { DownloadManager } from "./download-manager";
|
||||
import { watchProcesses } from "./process-watcher";
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export const startMainLoop = async () => {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
await Promise.allSettled([
|
||||
watchProcesses(),
|
||||
DownloadManager.watchDownloads(),
|
||||
]);
|
||||
|
||||
await sleep(500);
|
||||
}
|
||||
};
|
||||
@@ -5,73 +5,58 @@ import { gameRepository } from "@main/repository";
|
||||
import { getProcesses } from "@main/helpers";
|
||||
import { WindowManager } from "./window-manager";
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const gamesPlaytime = new Map<number, number>();
|
||||
|
||||
export const startProcessWatcher = async () => {
|
||||
const sleepTime = 500;
|
||||
const gamesPlaytime = new Map<number, number>();
|
||||
export const watchProcesses = async () => {
|
||||
const games = await gameRepository.find({
|
||||
where: {
|
||||
executablePath: Not(IsNull()),
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const games = await gameRepository.find({
|
||||
where: {
|
||||
executablePath: Not(IsNull()),
|
||||
isDeleted: false,
|
||||
},
|
||||
if (games.length === 0) return;
|
||||
|
||||
const processes = await getProcesses();
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
if (games.length === 0) {
|
||||
await sleep(sleepTime);
|
||||
continue;
|
||||
}
|
||||
if (gameProcess) {
|
||||
if (gamesPlaytime.has(game.id)) {
|
||||
const zero = gamesPlaytime.get(game.id) ?? 0;
|
||||
const delta = performance.now() - zero;
|
||||
|
||||
const processes = await getProcesses();
|
||||
|
||||
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
|
||||
);
|
||||
});
|
||||
|
||||
if (gameProcess) {
|
||||
if (gamesPlaytime.has(game.id)) {
|
||||
const zero = gamesPlaytime.get(game.id) ?? 0;
|
||||
const delta = performance.now() - zero;
|
||||
|
||||
if (WindowManager.mainWindow) {
|
||||
WindowManager.mainWindow.webContents.send("on-playtime", game.id);
|
||||
}
|
||||
|
||||
await gameRepository.update(game.id, {
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
});
|
||||
|
||||
gameRepository.update(game.id, {
|
||||
lastTimePlayed: new Date().toUTCString(),
|
||||
});
|
||||
}
|
||||
|
||||
gamesPlaytime.set(game.id, performance.now());
|
||||
} else if (gamesPlaytime.has(game.id)) {
|
||||
gamesPlaytime.delete(game.id);
|
||||
if (WindowManager.mainWindow) {
|
||||
WindowManager.mainWindow.webContents.send("on-game-close", game.id);
|
||||
WindowManager.mainWindow.webContents.send("on-playtime", game.id);
|
||||
}
|
||||
|
||||
await gameRepository.update(game.id, {
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
lastTimePlayed: new Date().toUTCString(),
|
||||
});
|
||||
}
|
||||
|
||||
gamesPlaytime.set(game.id, performance.now());
|
||||
} else if (gamesPlaytime.has(game.id)) {
|
||||
gamesPlaytime.delete(game.id);
|
||||
|
||||
if (WindowManager.mainWindow) {
|
||||
WindowManager.mainWindow.webContents.send("on-game-close", game.id);
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(sleepTime);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user