mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-28 13:21:02 +00:00
feat: adding aria2
This commit is contained in:
@@ -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!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./real-debrid.downloader";
|
||||
export * from "./torrent.downloader";
|
||||
@@ -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
|
||||
// );
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user