L=v%j(_v<)J(a_
zo*r&e&n&7eb<80h1o!XhoqJhBR)nLY_jED&&K5_rve?n=_^8&YAruVQ%`ztnW!y9Cjy_3kfH71zGs)U!=>tgQ9ZU$s-lqLaI5wrQE@g*Y*x?
zt{<_e;0^|MK?9?st~XIXiht!m~|17uT-<6?qCw;
z$~n>U#jA`t5$OSY;*19SVfz95&Gw*uj(xE0Gy1;=Y}eZ^wz+I)+a}nOtlwHcwLWS6
zopr6X!#dyUu$Eda>`1*6}%7RF7E
z%j2yLv!vn6S+{NpiJj#})vMROPR%7tw|z0)zHTicwg68joMFHIK=+7sYw9*0{nu@=Mt#$%4ucqT;!K{*sS=h$tj0Q5({|~RKq8H-b8(dxf7sQYcVaAdHZQ!!
zPnk|P6(OLAGst<4R^FVwGEF%#k{eDVn2FgD<@Gj1-;g3HJ7qtvuzvtlUKDF=!L3yEc!+sg4;mg5vHeSef@9G7xCcEr&c<^l8_4Eu3nc)`Hi5Zwm-IGt#!rO;fEeIMPKeW`nd{sjC|P!$m~;lczLeQ!joe^34o#LR|WBW{RO5+{C-{b|vU;%CQYMe_3U
zPR`M4Ey#RN{9fU>L*@StIZTuwTDm$YpV{N`1Ow~^2Hg6$Y6I#jwtw@!Pi5l8PGhs$
z8m8`H%=^}3_1(o0-(eh%IyJ!BEY%K)Mm7-AIL{SZ92@f;hT-fz8h%|d6TOPR!wA&q
z`Mb(ZnVE~&(a?9=^eJbhSu6R@B3VK|T*YC0bj8U9fNC;D>M^R<+=>A0D8-mM>=<)B
zrG8#sqgaV*(n=w>%w
z_Mc;WKKf3$G&2Kh4t*Vkvanc|TPUA+KRWlJN6FA-r+pVQsV_RZobBGz4wPpxg8$>H
zg+~`f|G6?`9J1}FG`oz>@S}v8*|FCju_)sxHaD?J3-!=e)cFHJP5x1ueUuax{pN-1
z;)cj~3XYPE7DV@Qot*Px8@iPL-y==5l9vV1c3OKfs+Y)eqgstKp^s4@#;bxyoop9onN
z8viB6ctP~_lES#0j4D4#p6J7pduacj@m~@;`wyKOw)gMJ{bCJupZ=}vWc=j|!eT>g
ztWxx`D*R%pyxO`eD&z8`;7I+V3G*UoFl62P_l)^Mf@JjY<&|-YR2=D_;gH1+R{K3J
z`Jn;zQ@2u<|5seEVZO(=QpKfONxl*bXh?_)6cK%6ocbv@&vk^|H22{^>zGf)F&bqJ
zM*kfK6n^-WieTq-yH?CSpq@|_tbVra?#fqr0eVhV4{>a6d%ano4Bl1QjenF3I&ul|tk(`@
zk7wZ*Eo?ZqjXr`sHqt@ptBO*(dDYeB%2ZhmBN)oMRM@w$OF2SEQ0wesqgRS_j_8dS
zhv)kIYxH(L-l>zy?|Zg3>`@c9@GR>?A2-~E&_eeC*8zvwV#a87J9Twh{19}_zB@$CW!9WV2S(UCjkal<~WvJ|?aHLz!#Ij3M4pU={
zsHoJdz2Jz$qa>Cc|Mf^?c4E$9ym40a{3}Kphb>cb4pUdpa(dOy`VcElz7?`RF26Z&
zx>&N#Ivh_Eh+E-7WGq?elT|={`!DO`R*p%op($c7sx!9BrvHqoj7@lc7lVQ-hp
z1YxS`#EiIf
z=sh0yoCZ~6?UV71-SpIULLci2O@8QRe~)lb&{v5%W!XOe-6cd7CzTjKC7`0J)1t|h
z>)Ft^y-PVmKp1dp`#U&q0lVZi%u#A?B`
zLpWODFOM8*=Yr8mXM2q!Mr0hqvnKZK>r-5^rO`x#>mY-|mrgamqDkG=w`ly9d9GNslAkxx!xApw)kXIQ(s_dbYD&
z#v0paIkS`EpcpUWzn#tcEqv4L$HHxCkzWOCSPkVI8c8OJM_C2ET^O
z;R?7Cu7a!K8n_m&gN<-KY=X_O1-8Nsunl_QM%WHJU?33w8of~Vmb*bje%
zXW=<`9$tVK;U#z(UV#JfD!c{<;dOWe-h@BFTkvOi8xFxc@GiUu@55i<1Naa=f{)=a
z9Dz^ZQ}`=<2A{*<;0yQ?zJkBQ*YFSc2L1{Ef`7yR!GGXeI10z$JNO=cfFI#FoPeL;
zXX91NP1h}t10$Fq9ugoCk{}t(V1X1!1uNLV4rwq5217bzzz`S;!ypr~ARBUEIE;Xi
zkP8ar!6?Xw(NF+mpb*AF5u5?zU_2DV1egdVFbPVb3?{=Am 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/entity/game.entity.ts b/src/main/entity/game.entity.ts
index 91e19ea6..fd168f51 100644
--- a/src/main/entity/game.entity.ts
+++ b/src/main/entity/game.entity.ts
@@ -10,7 +10,8 @@ import {
import { Repack } from "./repack.entity";
import type { GameShop } from "@types";
-import { Downloader, GameStatus } from "@shared";
+import { Downloader } from "@shared";
+import type { Aria2Status } from "aria2";
@Entity("game")
export class Game {
@@ -42,7 +43,7 @@ export class Game {
shop: GameShop;
@Column("text", { nullable: true })
- status: GameStatus | null;
+ status: Aria2Status | null;
@Column("int", { default: Downloader.Torrent })
downloader: Downloader;
@@ -53,9 +54,6 @@ export class Game {
@Column("float", { default: 0 })
progress: number;
- @Column("float", { default: 0 })
- fileVerificationProgress: number;
-
@Column("int", { default: 0 })
bytesDownloaded: number;
diff --git a/src/main/events/library/delete-game-folder.ts b/src/main/events/library/delete-game-folder.ts
index 954367a0..adfafefb 100644
--- a/src/main/events/library/delete-game-folder.ts
+++ b/src/main/events/library/delete-game-folder.ts
@@ -1,7 +1,6 @@
import path from "node:path";
import fs from "node:fs";
-import { GameStatus } from "@shared";
import { gameRepository } from "@main/repository";
import { getDownloadsPath } from "../helpers/get-downloads-path";
@@ -15,7 +14,7 @@ const deleteGameFolder = async (
const game = await gameRepository.findOne({
where: {
id: gameId,
- status: GameStatus.Cancelled,
+ status: "removed",
isDeleted: false,
},
});
diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts
index 2374c497..4fd4e254 100644
--- a/src/main/events/library/get-library.ts
+++ b/src/main/events/library/get-library.ts
@@ -2,7 +2,6 @@ import { gameRepository } from "@main/repository";
import { searchRepacks } from "../helpers/search-games";
import { registerEvent } from "../register-event";
-import { GameStatus } from "@shared";
import { sortBy } from "lodash-es";
const getLibrary = async () =>
@@ -24,7 +23,7 @@ const getLibrary = async () =>
...game,
repacks: searchRepacks(game.title),
})),
- (game) => (game.status !== GameStatus.Cancelled ? 0 : 1)
+ (game) => (game.status !== "removed" ? 0 : 1)
)
);
diff --git a/src/main/events/library/remove-game.ts b/src/main/events/library/remove-game.ts
index 57b10b37..54bf66b8 100644
--- a/src/main/events/library/remove-game.ts
+++ b/src/main/events/library/remove-game.ts
@@ -1,6 +1,5 @@
import { registerEvent } from "../register-event";
import { gameRepository } from "../../repository";
-import { GameStatus } from "@shared";
const removeGame = async (
_event: Electron.IpcMainInvokeEvent,
@@ -9,7 +8,7 @@ const removeGame = async (
await gameRepository.update(
{
id: gameId,
- status: GameStatus.Cancelled,
+ status: "removed",
},
{
status: null,
diff --git a/src/main/events/torrenting/cancel-game-download.ts b/src/main/events/torrenting/cancel-game-download.ts
index 18d29fde..3c9a0715 100644
--- a/src/main/events/torrenting/cancel-game-download.ts
+++ b/src/main/events/torrenting/cancel-game-download.ts
@@ -1,53 +1,25 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
-import { WindowManager } from "@main/services";
-import { In } from "typeorm";
import { DownloadManager } from "@main/services";
-import { GameStatus } from "@shared";
const cancelGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
- const game = await gameRepository.findOne({
- where: {
+ await DownloadManager.cancelDownload(gameId);
+
+ await gameRepository.update(
+ {
id: gameId,
- isDeleted: false,
- status: In([
- GameStatus.Downloading,
- GameStatus.DownloadingMetadata,
- GameStatus.CheckingFiles,
- GameStatus.Paused,
- GameStatus.Seeding,
- GameStatus.Finished,
- ]),
},
- });
-
- if (!game) return;
- DownloadManager.cancelDownload();
-
- await gameRepository
- .update(
- {
- id: game.id,
- },
- {
- status: GameStatus.Cancelled,
- bytesDownloaded: 0,
- progress: 0,
- }
- )
- .then((result) => {
- if (
- game.status !== GameStatus.Paused &&
- game.status !== GameStatus.Seeding
- ) {
- if (result.affected) WindowManager.mainWindow?.setProgressBar(-1);
- }
- });
+ {
+ status: "removed",
+ bytesDownloaded: 0,
+ progress: 0,
+ }
+ );
};
registerEvent("cancelGameDownload", cancelGameDownload);
diff --git a/src/main/events/torrenting/pause-game-download.ts b/src/main/events/torrenting/pause-game-download.ts
index ceda70cc..f9ed1102 100644
--- a/src/main/events/torrenting/pause-game-download.ts
+++ b/src/main/events/torrenting/pause-game-download.ts
@@ -1,30 +1,13 @@
import { registerEvent } from "../register-event";
import { gameRepository } from "../../repository";
-import { In } from "typeorm";
-import { DownloadManager, WindowManager } from "@main/services";
-import { GameStatus } from "@shared";
+import { DownloadManager } from "@main/services";
const pauseGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
- DownloadManager.pauseDownload();
-
- await gameRepository
- .update(
- {
- id: gameId,
- status: In([
- GameStatus.Downloading,
- GameStatus.DownloadingMetadata,
- GameStatus.CheckingFiles,
- ]),
- },
- { status: GameStatus.Paused }
- )
- .then((result) => {
- if (result.affected) WindowManager.mainWindow?.setProgressBar(-1);
- });
+ await DownloadManager.pauseDownload();
+ await gameRepository.update({ id: gameId }, { status: "paused" });
};
registerEvent("pauseGameDownload", pauseGameDownload);
diff --git a/src/main/events/torrenting/resume-game-download.ts b/src/main/events/torrenting/resume-game-download.ts
index 6982d895..51a81996 100644
--- a/src/main/events/torrenting/resume-game-download.ts
+++ b/src/main/events/torrenting/resume-game-download.ts
@@ -1,9 +1,7 @@
import { registerEvent } from "../register-event";
import { gameRepository } from "../../repository";
-import { getDownloadsPath } from "../helpers/get-downloads-path";
-import { In } from "typeorm";
+
import { DownloadManager } from "@main/services";
-import { GameStatus } from "@shared";
const resumeGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
@@ -18,31 +16,13 @@ const resumeGameDownload = async (
});
if (!game) return;
- DownloadManager.pauseDownload();
- if (game.status === GameStatus.Paused) {
- const downloadsPath = game.downloadPath ?? (await getDownloadsPath());
+ if (game.status === "paused") {
+ await DownloadManager.pauseDownload();
- DownloadManager.resumeDownload(gameId);
+ await gameRepository.update({ status: "active" }, { status: "paused" });
- await gameRepository.update(
- {
- status: In([
- GameStatus.Downloading,
- GameStatus.DownloadingMetadata,
- GameStatus.CheckingFiles,
- ]),
- },
- { status: GameStatus.Paused }
- );
-
- await gameRepository.update(
- { id: game.id },
- {
- status: GameStatus.Downloading,
- downloadPath: downloadsPath,
- }
- );
+ await DownloadManager.resumeDownload(gameId);
}
};
diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts
index f94d0999..62bce369 100644
--- a/src/main/events/torrenting/start-game-download.ts
+++ b/src/main/events/torrenting/start-game-download.ts
@@ -8,9 +8,8 @@ import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
-import { In } from "typeorm";
import { DownloadManager } from "@main/services";
-import { Downloader, GameStatus } from "@shared";
+import { Downloader } from "@shared";
import { stateManager } from "@main/state-manager";
const startGameDownload = async (
@@ -42,19 +41,9 @@ const startGameDownload = async (
}),
]);
- if (!repack || game?.status === GameStatus.Downloading) return;
- DownloadManager.pauseDownload();
+ if (!repack || game?.status === "active") return;
- await gameRepository.update(
- {
- status: In([
- GameStatus.Downloading,
- GameStatus.DownloadingMetadata,
- GameStatus.CheckingFiles,
- ]),
- },
- { status: GameStatus.Paused }
- );
+ await gameRepository.update({ status: "active" }, { status: "paused" });
if (game) {
await gameRepository.update(
@@ -62,17 +51,17 @@ const startGameDownload = async (
id: game.id,
},
{
- status: GameStatus.DownloadingMetadata,
- downloadPath: downloadPath,
+ status: "active",
+ downloadPath,
downloader,
repack: { id: repackId },
isDeleted: false,
}
);
- DownloadManager.downloadGame(game.id);
+ await DownloadManager.startDownload(game.id);
- game.status = GameStatus.DownloadingMetadata;
+ game.status = "active";
return game;
} else {
@@ -91,7 +80,7 @@ const startGameDownload = async (
objectID,
downloader,
shop: gameShop,
- status: GameStatus.Downloading,
+ status: "active",
downloadPath,
repack: { id: repackId },
})
@@ -105,7 +94,7 @@ const startGameDownload = async (
return result;
});
- DownloadManager.downloadGame(createdGame.id);
+ DownloadManager.startDownload(createdGame.id);
const { repack: _, ...rest } = createdGame;
diff --git a/src/main/main.ts b/src/main/main.ts
index e03a6ab8..a9f0ed19 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -13,17 +13,15 @@ import {
repackRepository,
userPreferencesRepository,
} from "./repository";
-import { TorrentDownloader } from "./services";
import { Repack, UserPreferences } from "./entity";
import { Notification } from "electron";
import { t } from "i18next";
-import { GameStatus } from "@shared";
-import { In } from "typeorm";
import fs from "node:fs";
import path from "node:path";
import { RealDebridClient } from "./services/real-debrid";
import { orderBy } from "lodash-es";
import { SteamGame } from "@types";
+import { Not } from "typeorm";
startProcessWatcher();
@@ -72,7 +70,7 @@ const checkForNewRepacks = async (userPreferences: UserPreferences | null) => {
};
const loadState = async (userPreferences: UserPreferences | null) => {
- const repacks = await repackRepository.find({
+ const repacks = repackRepository.find({
order: {
createdAt: "desc",
},
@@ -82,7 +80,7 @@ const loadState = async (userPreferences: UserPreferences | null) => {
fs.readFileSync(path.join(seedsPath, "steam-games.json"), "utf-8")
) as SteamGame[];
- stateManager.setValue("repacks", repacks);
+ stateManager.setValue("repacks", await repacks);
stateManager.setValue("steamGames", orderBy(steamGames, ["name"], "asc"));
import("./events");
@@ -90,22 +88,19 @@ const loadState = async (userPreferences: UserPreferences | null) => {
if (userPreferences?.realDebridApiToken)
await RealDebridClient.authorize(userPreferences?.realDebridApiToken);
+ await DownloadManager.connect();
+
const game = await gameRepository.findOne({
where: {
- status: In([
- GameStatus.Downloading,
- GameStatus.DownloadingMetadata,
- GameStatus.CheckingFiles,
- ]),
+ status: "active",
+ progress: Not(1),
isDeleted: false,
},
relations: { repack: true },
});
- await TorrentDownloader.startClient();
-
if (game) {
- DownloadManager.resumeDownload(game.id);
+ DownloadManager.startDownload(game.id);
}
};
diff --git a/src/main/services/download-manager.ts b/src/main/services/download-manager.ts
index e345835a..94e19835 100644
--- a/src/main/services/download-manager.ts
+++ b/src/main/services/download-manager.ts
@@ -1,13 +1,156 @@
-import { gameRepository } from "@main/repository";
+import Aria2, { StatusResponse } from "aria2";
+import { spawn } from "node:child_process";
-import type { Game } from "@main/entity";
+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 { t } from "i18next";
import { Downloader } from "@shared";
-
-import { writePipe } from "./fifo";
-import { RealDebridDownloader } from "./downloaders";
+import { DownloadProgress } from "@types";
export class DownloadManager {
- private static gameDownloading: Game;
+ private static downloads = new Map();
+
+ 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"
+ );
+
+ spawn(binary, ["--enable-rpc", "--rpc-listen-all"], { stdio: "inherit" });
+
+ await this.aria2.open();
+ this.attachListener();
+ }
+
+ private static getETA(status: StatusResponse) {
+ const remainingBytes =
+ Number(status.totalLength) - Number(status.completedLength);
+ const speed = Number(status.downloadSpeed);
+
+ if (remainingBytes >= 0 && speed > 0) {
+ return (remainingBytes / speed) * 1000;
+ }
+
+ return -1;
+ }
+
+ static async publishNotification() {
+ const userPreferences = await userPreferencesRepository.findOne({
+ where: { id: 1 },
+ });
+
+ if (userPreferences?.downloadNotificationsEnabled && this.gameId) {
+ const game = await this.getGame(this.gameId);
+
+ new Notification({
+ title: t("download_complete", {
+ ns: "notifications",
+ lng: userPreferences.language,
+ }),
+ body: t("game_ready_to_install", {
+ ns: "notifications",
+ lng: userPreferences.language,
+ title: game?.title,
+ }),
+ }).show();
+ }
+ }
+
+ private static getFolderName(status: StatusResponse) {
+ if (status.bittorrent?.info) return status.bittorrent.info.name;
+ return "";
+ }
+
+ private static async attachListener() {
+ while (true) {
+ try {
+ if (!this.gid || !this.gameId) {
+ continue;
+ }
+
+ 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);
+ continue;
+ }
+
+ const progress =
+ Number(status.completedLength) / Number(status.totalLength);
+
+ await gameRepository.update(
+ { id: this.gameId },
+ {
+ progress:
+ isNaN(progress) || downloadingMetadata ? undefined : progress,
+ bytesDownloaded: Number(status.completedLength),
+ fileSize: Number(status.totalLength),
+ 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));
+ }
+ }
+ }
static async getGame(gameId: number) {
return gameRepository.findOne({
@@ -18,59 +161,80 @@ export class DownloadManager {
});
}
- static async cancelDownload() {
- if (
- this.gameDownloading &&
- this.gameDownloading.downloader === Downloader.Torrent
- ) {
- writePipe.write({ action: "cancel" });
- } else {
- RealDebridDownloader.destroy();
+ private static clearCurrentDownload() {
+ if (this.gameId) {
+ this.downloads.delete(this.gameId);
+ this.gid = null;
+ this.gameId = null;
+ }
+ }
+
+ static async cancelDownload(gameId: number) {
+ const gid = this.downloads.get(gameId);
+
+ if (gid) {
+ await this.aria2.call("remove", gid);
+
+ if (this.gid === gid) {
+ this.clearCurrentDownload();
+
+ WindowManager.mainWindow?.setProgressBar(-1);
+ } else {
+ this.downloads.delete(gameId);
+ }
}
}
static async pauseDownload() {
- if (
- this.gameDownloading &&
- this.gameDownloading.downloader === Downloader.Torrent
- ) {
- writePipe.write({ action: "pause" });
- } else {
- RealDebridDownloader.destroy();
+ if (this.gid) {
+ await this.aria2.call("forcePause", this.gid);
+ this.gid = null;
+ this.gameId = null;
+
+ WindowManager.mainWindow?.setProgressBar(-1);
}
}
static async resumeDownload(gameId: number) {
- const game = await this.getGame(gameId);
+ await this.aria2.call("forcePauseAll");
- if (game!.downloader === Downloader.Torrent) {
- writePipe.write({
- action: "start",
- game_id: game!.id,
- magnet: game!.repack.magnet,
- save_path: game!.downloadPath,
- });
+ if (this.downloads.has(gameId)) {
+ const gid = this.downloads.get(gameId)!;
+ await this.aria2.call("unpause", gid);
+
+ this.gid = gid;
+ this.gameId = gameId;
} else {
- RealDebridDownloader.startDownload(game!);
+ return this.startDownload(gameId);
}
-
- this.gameDownloading = game!;
}
- static async downloadGame(gameId: number) {
- const game = await this.getGame(gameId);
+ static async startDownload(gameId: number) {
+ await this.aria2.call("forcePauseAll");
- if (game!.downloader === Downloader.Torrent) {
- writePipe.write({
- action: "start",
- game_id: game!.id,
- magnet: game!.repack.magnet,
- save_path: game!.downloadPath,
- });
- } else {
- RealDebridDownloader.startDownload(game!);
+ const game = await this.getGame(gameId)!;
+
+ if (game) {
+ const options = {
+ dir: game.downloadPath!,
+ };
+
+ if (game.downloader === Downloader.RealDebrid) {
+ const downloadUrl = decodeURIComponent(
+ await RealDebridClient.getDownloadUrl(game)
+ );
+
+ this.gid = await this.aria2.call("addUri", [downloadUrl], options);
+ } else {
+ this.gid = await this.aria2.call(
+ "addUri",
+ [game.repack.magnet],
+ options
+ );
+ }
+
+ this.gameId = gameId;
+ this.downloads.set(gameId, this.gid);
}
-
- this.gameDownloading = game!;
}
}
diff --git a/src/main/services/downloaders/downloader.ts b/src/main/services/downloaders/downloader.ts
deleted file mode 100644
index 14440676..00000000
--- a/src/main/services/downloaders/downloader.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { t } from "i18next";
-import { Notification } from "electron";
-
-import { Game } from "@main/entity";
-
-import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
-
-import { WindowManager } from "../window-manager";
-import type { TorrentUpdate } from "./torrent.downloader";
-
-import { GameStatus } from "@shared";
-import { gameRepository, userPreferencesRepository } from "@main/repository";
-
-interface DownloadStatus {
- numPeers?: number;
- numSeeds?: number;
- downloadSpeed?: number;
- timeRemaining?: number;
-}
-
-export class Downloader {
- static getGameProgress(game: Game) {
- if (game.status === GameStatus.CheckingFiles)
- return game.fileVerificationProgress;
-
- return game.progress;
- }
-
- static async updateGameProgress(
- gameId: number,
- gameUpdate: QueryDeepPartialEntity,
- downloadStatus: DownloadStatus
- ) {
- await gameRepository.update({ id: gameId }, gameUpdate);
-
- const game = await gameRepository.findOne({
- where: { id: gameId, isDeleted: false },
- relations: { repack: true },
- });
-
- if (game?.progress === 1) {
- const userPreferences = await userPreferencesRepository.findOne({
- where: { id: 1 },
- });
-
- if (userPreferences?.downloadNotificationsEnabled) {
- new Notification({
- title: t("download_complete", {
- ns: "notifications",
- lng: userPreferences.language,
- }),
- body: t("game_ready_to_install", {
- ns: "notifications",
- lng: userPreferences.language,
- title: game?.title,
- }),
- }).show();
- }
- }
-
- if (WindowManager.mainWindow && game) {
- const progress = this.getGameProgress(game);
- WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
-
- WindowManager.mainWindow.webContents.send(
- "on-download-progress",
- JSON.parse(
- JSON.stringify({
- ...({
- progress: gameUpdate.progress,
- bytesDownloaded: gameUpdate.bytesDownloaded,
- fileSize: gameUpdate.fileSize,
- gameId,
- numPeers: downloadStatus.numPeers,
- numSeeds: downloadStatus.numSeeds,
- downloadSpeed: downloadStatus.downloadSpeed,
- timeRemaining: downloadStatus.timeRemaining,
- } as TorrentUpdate),
- game,
- })
- )
- );
- }
- }
-}
diff --git a/src/main/services/downloaders/index.ts b/src/main/services/downloaders/index.ts
deleted file mode 100644
index cd742107..00000000
--- a/src/main/services/downloaders/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./real-debrid.downloader";
-export * from "./torrent.downloader";
diff --git a/src/main/services/downloaders/real-debrid.downloader.ts b/src/main/services/downloaders/real-debrid.downloader.ts
deleted file mode 100644
index 8a44f934..00000000
--- a/src/main/services/downloaders/real-debrid.downloader.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import { Game } from "@main/entity";
-import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
-import path from "node:path";
-import fs from "node:fs";
-import EasyDL from "easydl";
-import { GameStatus } from "@shared";
-// import { fullArchive } from "node-7z-archive";
-
-import { Downloader } from "./downloader";
-import { RealDebridClient } from "../real-debrid";
-
-export class RealDebridDownloader extends Downloader {
- private static download: EasyDL;
- private static downloadSize = 0;
-
- private static getEta(bytesDownloaded: number, speed: number) {
- const remainingBytes = this.downloadSize - bytesDownloaded;
-
- if (remainingBytes >= 0 && speed > 0) {
- return (remainingBytes / speed) * 1000;
- }
-
- return 1;
- }
-
- private static createFolderIfNotExists(path: string) {
- if (!fs.existsSync(path)) {
- fs.mkdirSync(path);
- }
- }
-
- // private static async startDecompression(
- // rarFile: string,
- // dest: string,
- // game: Game
- // ) {
- // await fullArchive(rarFile, dest);
-
- // const updatePayload: QueryDeepPartialEntity = {
- // status: GameStatus.Finished,
- // };
-
- // await this.updateGameProgress(game.id, updatePayload, {});
- // }
-
- static destroy() {
- if (this.download) {
- this.download.destroy();
- }
- }
-
- static async startDownload(game: Game) {
- if (this.download) this.download.destroy();
- const downloadUrl = decodeURIComponent(
- await RealDebridClient.getDownloadUrl(game)
- );
-
- const filename = path.basename(downloadUrl);
- const folderName = path.basename(filename, path.extname(filename));
-
- const downloadPath = path.join(game.downloadPath!, folderName);
- this.createFolderIfNotExists(downloadPath);
-
- this.download = new EasyDL(downloadUrl, path.join(downloadPath, filename));
-
- const metadata = await this.download.metadata();
-
- this.downloadSize = metadata.size;
-
- const updatePayload: QueryDeepPartialEntity = {
- status: GameStatus.Downloading,
- fileSize: metadata.size,
- folderName,
- };
-
- const downloadStatus = {
- timeRemaining: Number.POSITIVE_INFINITY,
- };
-
- await this.updateGameProgress(game.id, updatePayload, downloadStatus);
-
- this.download.on("progress", async ({ total }) => {
- const updatePayload: QueryDeepPartialEntity = {
- status: GameStatus.Downloading,
- progress: Math.min(0.99, total.percentage / 100),
- bytesDownloaded: total.bytes,
- };
-
- const downloadStatus = {
- downloadSpeed: total.speed,
- timeRemaining: this.getEta(total.bytes ?? 0, total.speed ?? 0),
- };
-
- await this.updateGameProgress(game.id, updatePayload, downloadStatus);
- });
-
- this.download.on("end", async () => {
- const updatePayload: QueryDeepPartialEntity = {
- status: GameStatus.Finished,
- progress: 1,
- };
-
- await this.updateGameProgress(game.id, updatePayload, {
- timeRemaining: 0,
- });
-
- /* This has to be improved */
- // this.startDecompression(
- // path.join(downloadPath, filename),
- // downloadPath,
- // game
- // );
- });
- }
-}
diff --git a/src/main/services/downloaders/torrent.downloader.ts b/src/main/services/downloaders/torrent.downloader.ts
deleted file mode 100644
index d5e039a8..00000000
--- a/src/main/services/downloaders/torrent.downloader.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-import path from "node:path";
-import cp from "node:child_process";
-import fs from "node:fs";
-import { app, dialog } from "electron";
-import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
-
-import { Game } from "@main/entity";
-import { GameStatus } from "@shared";
-import { Downloader } from "./downloader";
-import { readPipe, writePipe } from "../fifo";
-
-const binaryNameByPlatform: Partial> = {
- darwin: "hydra-download-manager",
- linux: "hydra-download-manager",
- win32: "hydra-download-manager.exe",
-};
-
-enum TorrentState {
- CheckingFiles = 1,
- DownloadingMetadata = 2,
- Downloading = 3,
- Finished = 4,
- Seeding = 5,
-}
-
-export interface TorrentUpdate {
- gameId: number;
- progress: number;
- downloadSpeed: number;
- timeRemaining: number;
- numPeers: number;
- numSeeds: number;
- status: TorrentState;
- folderName: string;
- fileSize: number;
- bytesDownloaded: number;
-}
-
-export const BITTORRENT_PORT = "5881";
-
-export class TorrentDownloader extends Downloader {
- private static messageLength = 1024 * 2;
-
- public static async attachListener() {
- // eslint-disable-next-line no-constant-condition
- while (true) {
- const buffer = readPipe.socket?.read(this.messageLength);
-
- if (buffer === null) {
- await new Promise((resolve) => setTimeout(resolve, 100));
- continue;
- }
-
- const message = Buffer.from(
- buffer.slice(0, buffer.indexOf(0x00))
- ).toString("utf-8");
-
- try {
- const payload = JSON.parse(message) as TorrentUpdate;
-
- const updatePayload: QueryDeepPartialEntity = {
- bytesDownloaded: payload.bytesDownloaded,
- status: this.getTorrentStateName(payload.status),
- };
-
- if (payload.status === TorrentState.CheckingFiles) {
- updatePayload.fileVerificationProgress = payload.progress;
- } else {
- if (payload.folderName) {
- updatePayload.folderName = payload.folderName;
- updatePayload.fileSize = payload.fileSize;
- }
- }
-
- if (
- [TorrentState.Downloading, TorrentState.Seeding].includes(
- payload.status
- )
- ) {
- updatePayload.progress = payload.progress;
- }
-
- this.updateGameProgress(payload.gameId, updatePayload, {
- numPeers: payload.numPeers,
- numSeeds: payload.numSeeds,
- downloadSpeed: payload.downloadSpeed,
- timeRemaining: payload.timeRemaining,
- });
- } finally {
- await new Promise((resolve) => setTimeout(resolve, 100));
- }
- }
- }
-
- public static startClient() {
- return new Promise((resolve) => {
- const commonArgs = [
- BITTORRENT_PORT,
- writePipe.socketPath,
- readPipe.socketPath,
- ];
-
- if (app.isPackaged) {
- const binaryName = binaryNameByPlatform[process.platform]!;
- const binaryPath = path.join(
- process.resourcesPath,
- "hydra-download-manager",
- binaryName
- );
-
- if (!fs.existsSync(binaryPath)) {
- dialog.showErrorBox(
- "Fatal",
- "Hydra download manager binary not found. Please check if it has been removed by Windows Defender."
- );
-
- app.quit();
- }
-
- cp.spawn(binaryPath, commonArgs, {
- stdio: "inherit",
- windowsHide: true,
- });
- } else {
- const scriptPath = path.join(
- __dirname,
- "..",
- "..",
- "torrent-client",
- "main.py"
- );
-
- cp.spawn("python3", [scriptPath, ...commonArgs], {
- stdio: "inherit",
- });
- }
-
- Promise.all([writePipe.createPipe(), readPipe.createPipe()]).then(
- async () => {
- this.attachListener();
- resolve(null);
- }
- );
- });
- }
-
- private static getTorrentStateName(state: TorrentState) {
- if (state === TorrentState.CheckingFiles) return GameStatus.CheckingFiles;
- if (state === TorrentState.Downloading) return GameStatus.Downloading;
- if (state === TorrentState.DownloadingMetadata)
- return GameStatus.DownloadingMetadata;
- if (state === TorrentState.Finished) return GameStatus.Finished;
- if (state === TorrentState.Seeding) return GameStatus.Seeding;
- return null;
- }
-}
diff --git a/src/main/services/fifo.ts b/src/main/services/fifo.ts
deleted file mode 100644
index 866232cc..00000000
--- a/src/main/services/fifo.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import path from "node:path";
-import net from "node:net";
-import crypto from "node:crypto";
-import os from "node:os";
-
-export class FIFO {
- public socket: null | net.Socket = null;
- public socketPath = this.generateSocketFilename();
-
- private generateSocketFilename() {
- const hash = crypto.randomBytes(16).toString("hex");
-
- if (process.platform === "win32") {
- return "\\\\.\\pipe\\" + hash;
- }
-
- return path.join(os.tmpdir(), hash);
- }
-
- public write(data: any) {
- if (!this.socket) return;
- this.socket.write(Buffer.from(JSON.stringify(data)));
- }
-
- public createPipe() {
- return new Promise((resolve) => {
- const server = net.createServer((socket) => {
- this.socket = socket;
- resolve(null);
- });
-
- server.listen(this.socketPath);
- });
- }
-}
-
-export const writePipe = new FIFO();
-export const readPipe = new FIFO();
diff --git a/src/main/services/index.ts b/src/main/services/index.ts
index 4b13d38d..4808736d 100644
--- a/src/main/services/index.ts
+++ b/src/main/services/index.ts
@@ -5,8 +5,6 @@ export * from "./steam-250";
export * from "./steam-grid";
export * from "./update-resolver";
export * from "./window-manager";
-export * from "./fifo";
-export * from "./downloaders";
export * from "./download-manager";
export * from "./how-long-to-beat";
export * from "./process-watcher";
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 6a209787..0e397a4a 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -5,7 +5,7 @@ import { contextBridge, ipcRenderer } from "electron";
import type {
CatalogueCategory,
GameShop,
- TorrentProgress,
+ DownloadProgress,
UserPreferences,
} from "@types";
@@ -32,10 +32,10 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("pauseGameDownload", gameId),
resumeGameDownload: (gameId: number) =>
ipcRenderer.invoke("resumeGameDownload", gameId),
- onDownloadProgress: (cb: (value: TorrentProgress) => void) => {
+ onDownloadProgress: (cb: (value: DownloadProgress) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
- value: TorrentProgress
+ value: DownloadProgress
) => cb(value);
ipcRenderer.on("on-download-progress", listener);
return () => ipcRenderer.removeListener("on-download-progress", listener);
diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx
index da95f292..adb2a613 100644
--- a/src/renderer/src/app.tsx
+++ b/src/renderer/src/app.tsx
@@ -19,7 +19,6 @@ import {
setUserPreferences,
toggleDraggingDisabled,
} from "@renderer/features";
-import { GameStatusHelper } from "@shared";
document.body.classList.add(themeClass);
@@ -54,7 +53,7 @@ export function App({ children }: AppProps) {
useEffect(() => {
const unsubscribe = window.electron.onDownloadProgress(
(downloadProgress) => {
- if (GameStatusHelper.isReady(downloadProgress.game.status)) {
+ if (downloadProgress.game.progress === 1) {
clearDownload();
updateLibrary();
return;
diff --git a/src/renderer/src/components/backdrop/backdrop.css.ts b/src/renderer/src/components/backdrop/backdrop.css.ts
index 0a7b61bb..3b8cc4e2 100644
--- a/src/renderer/src/components/backdrop/backdrop.css.ts
+++ b/src/renderer/src/components/backdrop/backdrop.css.ts
@@ -43,5 +43,11 @@ export const backdrop = recipe({
backgroundColor: "rgba(0, 0, 0, 0)",
},
},
+ windows: {
+ true: {
+ // SPACING_UNIT * 3 + title bar spacing
+ paddingTop: `${SPACING_UNIT * 3 + 35}px`,
+ },
+ },
},
});
diff --git a/src/renderer/src/components/backdrop/backdrop.tsx b/src/renderer/src/components/backdrop/backdrop.tsx
index 5852d59d..f498e664 100644
--- a/src/renderer/src/components/backdrop/backdrop.tsx
+++ b/src/renderer/src/components/backdrop/backdrop.tsx
@@ -7,6 +7,13 @@ export interface BackdropProps {
export function Backdrop({ isClosing = false, children }: BackdropProps) {
return (
- {children}
+
+ {children}
+
);
}
diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx
index 44d125cd..310f31b4 100644
--- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx
+++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx
@@ -7,17 +7,16 @@ import { vars } from "../../theme.css";
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { VERSION_CODENAME } from "@renderer/constants";
-import { GameStatus, GameStatusHelper } from "@shared";
export function BottomPanel() {
const { t } = useTranslation("bottom_panel");
const navigate = useNavigate();
- const { game, progress, downloadSpeed, eta } = useDownload();
+ const { lastPacket, progress, downloadSpeed, eta } = useDownload();
const isGameDownloading =
- game && GameStatusHelper.isDownloading(game.status ?? null);
+ lastPacket?.game && lastPacket?.game.status === "active";
const [version, setVersion] = useState("");
@@ -27,17 +26,8 @@ export function BottomPanel() {
const status = useMemo(() => {
if (isGameDownloading) {
- if (game.status === GameStatus.DownloadingMetadata)
- return t("downloading_metadata", { title: game.title });
-
- if (game.status === GameStatus.CheckingFiles)
- return t("checking_files", {
- title: game.title,
- percentage: progress,
- });
-
return t("downloading", {
- title: game?.title,
+ title: lastPacket?.game.title,
percentage: progress,
eta,
speed: downloadSpeed,
@@ -45,7 +35,7 @@ export function BottomPanel() {
}
return t("no_downloads_in_progress");
- }, [t, isGameDownloading, game, progress, eta, downloadSpeed]);
+ }, [t, isGameDownloading, lastPacket?.game, progress, eta, downloadSpeed]);
return (
- {isGamePlaying ? (
+ {isGameRunning ? (
{t("playing_now")}
) : (
diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.tsx b/src/renderer/src/pages/game-details/hero/hero-panel.tsx
index 87a4b0ee..5f4ba9d1 100644
--- a/src/renderer/src/pages/game-details/hero/hero-panel.tsx
+++ b/src/renderer/src/pages/game-details/hero/hero-panel.tsx
@@ -1,72 +1,48 @@
import { format } from "date-fns";
-import { useMemo, useState } from "react";
+import { useContext, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
+import Color from "color";
import { useDownload } from "@renderer/hooks";
-import type { Game, GameRepack } from "@types";
import { formatDownloadProgress } from "@renderer/helpers";
import { HeroPanelActions } from "./hero-panel-actions";
-import { Downloader, GameStatus, GameStatusHelper, formatBytes } from "@shared";
+import { Downloader, formatBytes } from "@shared";
import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal";
import * as styles from "./hero-panel.css";
import { HeroPanelPlaytime } from "./hero-panel-playtime";
+import { gameDetailsContext } from "../game-details.context";
-export interface HeroPanelProps {
- game: Game | null;
- color: string;
- isGamePlaying: boolean;
- objectID: string;
- title: string;
- repacks: GameRepack[];
- openRepacksModal: () => void;
- getGame: () => void;
-}
-
-export function HeroPanel({
- game,
- color,
- repacks,
- objectID,
- title,
- isGamePlaying,
- openRepacksModal,
- getGame,
-}: HeroPanelProps) {
+export function HeroPanel() {
const { t } = useTranslation("game_details");
+ const { game, repacks, gameColor } = useContext(gameDetailsContext);
+
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
- const {
- game: gameDownloading,
- progress,
- eta,
- numPeers,
- numSeeds,
- isGameDeleting,
- } = useDownload();
-
- const isGameDownloading =
- gameDownloading?.id === game?.id &&
- GameStatusHelper.isDownloading(game?.status ?? null);
+ const { progress, eta, lastPacket, isGameDeleting } = useDownload();
const finalDownloadSize = useMemo(() => {
if (!game) return "N/A";
if (game.fileSize) return formatBytes(game.fileSize);
- if (gameDownloading?.fileSize && isGameDownloading)
- return formatBytes(gameDownloading.fileSize);
+ if (lastPacket?.game.fileSize && game?.status === "active")
+ return formatBytes(lastPacket?.game.fileSize);
return game.repack?.fileSize ?? "N/A";
- }, [game, isGameDownloading, gameDownloading]);
+ }, [game, lastPacket?.game]);
const getInfo = () => {
- if (isGameDeleting(game?.id ?? -1)) {
- return
{t("deleting")}
;
- }
+ if (isGameDeleting(game?.id ?? -1)) return {t("deleting")}
;
+
+ if (game?.progress === 1) return ;
+
+ if (game?.status === "active") {
+ if (lastPacket?.downloadingMetadata) {
+ return {t("downloading_metadata")}
;
+ }
- if (isGameDownloading && gameDownloading?.status) {
return (
<>
@@ -74,33 +50,25 @@ export function HeroPanel({
{eta && {t("eta", { eta })}}
- {gameDownloading.status !== GameStatus.Downloading ? (
- <>
- {t(gameDownloading.status)}
- {eta && {t("eta", { eta })}}
- >
- ) : (
-
- {formatBytes(gameDownloading.bytesDownloaded)} /{" "}
- {finalDownloadSize}
+
+ {formatBytes(lastPacket?.game?.bytesDownloaded ?? 0)} /{" "}
+ {finalDownloadSize}
+ {game?.downloader === Downloader.Torrent && (
- {game?.downloader === Downloader.Torrent &&
- `${numPeers} peers / ${numSeeds} seeds`}
+ {lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
-
- )}
+ )}
+