feat: adding aria2

This commit is contained in:
Chubby Granny Chaser
2024-05-20 02:21:11 +01:00
parent a89e6760da
commit 4941709296
58 changed files with 895 additions and 1329 deletions

80
src/main/declaration.d.ts vendored Normal file
View File

@@ -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<void>;
call(
method: "addUri",
uris: string[],
options: { dir: string }
): Promise<string>;
call(
method: "tellStatus",
gid: string,
keys?: string[]
): Promise<StatusResponse>;
call(method: "pause", gid: string): Promise<string>;
call(method: "forcePause", gid: string): Promise<string>;
call(method: "unpause", gid: string): Promise<string>;
call(method: "remove", gid: string): Promise<string>;
call(method: "forceRemove", gid: string): Promise<string>;
call(method: "pauseAll"): Promise<string>;
call(method: "forcePauseAll"): Promise<string>;
listNotifications: () => [
"onDownloadStart",
"onDownloadPause",
"onDownloadStop",
"onDownloadComplete",
"onDownloadError",
"onBtDownloadComplete",
];
on: (event: string, callback: (params: any) => void) => void;
}
}

View File

@@ -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;

View File

@@ -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,
},
});

View File

@@ -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)
)
);

View File

@@ -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,

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}
};

View File

@@ -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;

View File

@@ -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);
}
};

View File

@@ -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<number, string>();
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!;
}
}

View File

@@ -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<Game>,
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,
})
)
);
}
}
}

View File

@@ -1,2 +0,0 @@
export * from "./real-debrid.downloader";
export * from "./torrent.downloader";

View File

@@ -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<Game> = {
// 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<Game> = {
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<Game> = {
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<Game> = {
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
// );
});
}
}

View File

@@ -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<Record<NodeJS.Platform, string>> = {
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<Game> = {
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;
}
}

View File

@@ -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();

View File

@@ -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";