Merge branch 'main' into linux-install

This commit is contained in:
Zamitto
2024-05-29 22:21:52 -03:00
committed by GitHub
136 changed files with 2430 additions and 2533 deletions

View File

@@ -26,6 +26,8 @@ export const databasePath = path.join(
"hydra.db"
);
export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");
export const seedsPath = app.isPackaged
? path.join(process.resourcesPath, "seeds")
: path.join(__dirname, "..", "..", "seeds");

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,13 +54,10 @@ export class Game {
@Column("float", { default: 0 })
progress: number;
@Column("float", { default: 0 })
fileVerificationProgress: number;
@Column("int", { default: 0 })
bytesDownloaded: number;
@Column("text", { nullable: true })
@Column("datetime", { nullable: true })
lastTimePlayed: Date | null;
@Column("float", { default: 0 })

View File

@@ -1,47 +1,32 @@
import { AppUpdaterEvents } from "@types";
import { registerEvent } from "../register-event";
import updater, { ProgressInfo, UpdateInfo } from "electron-updater";
import updater, { UpdateInfo } from "electron-updater";
import { WindowManager } from "@main/services";
import { app } from "electron";
const { autoUpdater } = updater;
const sendEvent = (event: AppUpdaterEvents) => {
WindowManager.splashWindow?.webContents.send("autoUpdaterEvent", event);
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
};
const mockValuesForDebug = async () => {
sendEvent({ type: "update-downloaded" });
const mockValuesForDebug = () => {
sendEvent({ type: "update-available", info: { version: "1.3.0" } });
};
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
autoUpdater
.addListener("error", () => {
sendEvent({ type: "error" });
})
.addListener("checking-for-update", () => {
sendEvent({ type: "checking-for-updates" });
})
.addListener("update-not-available", () => {
sendEvent({ type: "update-not-available" });
})
.addListener("update-available", (info: UpdateInfo) => {
.once("update-available", (info: UpdateInfo) => {
sendEvent({ type: "update-available", info });
})
.addListener("update-downloaded", () => {
.once("update-downloaded", () => {
sendEvent({ type: "update-downloaded" });
})
.addListener("download-progress", (info: ProgressInfo) => {
sendEvent({ type: "download-progress", info });
})
.addListener("update-cancelled", () => {
sendEvent({ type: "update-cancelled" });
});
if (app.isPackaged) {
autoUpdater.checkForUpdates();
} else {
await mockValuesForDebug();
mockValuesForDebug();
}
};

View File

@@ -1,12 +0,0 @@
import { WindowManager } from "@main/services";
import { registerEvent } from "../register-event";
import updater from "electron-updater";
const { autoUpdater } = updater;
const continueToMainWindow = async (_event: Electron.IpcMainInvokeEvent) => {
autoUpdater.removeAllListeners();
WindowManager.prepareMainWindowAndCloseSplash();
};
registerEvent("continueToMainWindow", continueToMainWindow);

View File

@@ -1,16 +1,13 @@
import { app } from "electron";
import { registerEvent } from "../register-event";
import updater from "electron-updater";
import { WindowManager } from "@main/services";
const { autoUpdater } = updater;
const restartAndInstallUpdate = async (_event: Electron.IpcMainInvokeEvent) => {
autoUpdater.removeAllListeners();
if (app.isPackaged) {
autoUpdater.quitAndInstall(true, true);
} else {
autoUpdater.removeAllListeners();
WindowManager.prepareMainWindowAndCloseSplash();
}
};

View File

@@ -29,7 +29,7 @@ import "./user-preferences/update-user-preferences";
import "./user-preferences/auto-launch";
import "./autoupdater/check-for-updates";
import "./autoupdater/restart-and-install-update";
import "./autoupdater/continue-to-main-window";
import "./user-preferences/authenticate-real-debrid";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => app.getVersion());

View File

@@ -10,7 +10,7 @@ const addGameToLibrary = async (
_event: Electron.IpcMainInvokeEvent,
objectID: string,
title: string,
gameShop: GameShop,
shop: GameShop,
executablePath: string | null
) => {
return gameRepository
@@ -19,7 +19,7 @@ const addGameToLibrary = async (
objectID,
},
{
shop: gameShop,
shop,
status: null,
executablePath,
isDeleted: false,
@@ -40,7 +40,7 @@ const addGameToLibrary = async (
title,
iconUrl,
objectID,
shop: gameShop,
shop,
executablePath,
})
.then(() => {

View File

@@ -1,7 +1,8 @@
import path from "node:path";
import fs from "node:fs";
import { GameStatus } from "@shared";
import { In } from "typeorm";
import { gameRepository } from "@main/repository";
import { getDownloadsPath } from "../helpers/get-downloads-path";
@@ -15,7 +16,7 @@ const deleteGameFolder = async (
const game = await gameRepository.findOne({
where: {
id: gameId,
status: GameStatus.Cancelled,
status: In(["removed", "complete"]),
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,10 +8,9 @@ const removeGame = async (
await gameRepository.update(
{
id: gameId,
status: GameStatus.Cancelled,
},
{
status: null,
status: "removed",
downloadPath: null,
bytesDownloaded: 0,
progress: 0,

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,11 @@
import { Not } from "typeorm";
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";
import { dataSource } from "@main/data-source";
import { Game } from "@main/entity";
const resumeGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
@@ -18,31 +20,21 @@ const resumeGameDownload = async (
});
if (!game) return;
DownloadManager.pauseDownload();
if (game.status === GameStatus.Paused) {
const downloadsPath = game.downloadPath ?? (await getDownloadsPath());
if (game.status === "paused") {
await dataSource.transaction(async (transactionalEntityManager) => {
await DownloadManager.pauseDownload();
DownloadManager.resumeDownload(gameId);
await transactionalEntityManager
.getRepository(Game)
.update({ status: "active", progress: Not(1) }, { status: "paused" });
await gameRepository.update(
{
status: In([
GameStatus.Downloading,
GameStatus.DownloadingMetadata,
GameStatus.CheckingFiles,
]),
},
{ status: GameStatus.Paused }
);
await DownloadManager.resumeDownload(game);
await gameRepository.update(
{ id: game.id },
{
status: GameStatus.Downloading,
downloadPath: downloadsPath,
}
);
await transactionalEntityManager
.getRepository(Game)
.update({ id: gameId }, { status: "active" });
});
}
};

View File

@@ -1,39 +1,26 @@
import {
gameRepository,
repackRepository,
userPreferencesRepository,
} from "@main/repository";
import { gameRepository, repackRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
import type { StartGameDownloadPayload } from "@types";
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
import { In } from "typeorm";
import { DownloadManager } from "@main/services";
import { Downloader, GameStatus } from "@shared";
import { stateManager } from "@main/state-manager";
import { Not } from "typeorm";
const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
repackId: number,
objectID: string,
title: string,
gameShop: GameShop,
downloadPath: string
payload: StartGameDownloadPayload
) => {
const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 },
});
const downloader = userPreferences?.realDebridApiToken
? Downloader.RealDebrid
: Downloader.Torrent;
const { repackId, objectID, title, shop, downloadPath, downloader } = payload;
const [game, repack] = await Promise.all([
gameRepository.findOne({
where: {
objectID,
shop,
},
relations: { repack: true },
}),
repackRepository.findOne({
where: {
@@ -42,18 +29,13 @@ const startGameDownload = async (
}),
]);
if (!repack || game?.status === GameStatus.Downloading) return;
DownloadManager.pauseDownload();
if (!repack) return;
await DownloadManager.pauseDownload();
await gameRepository.update(
{
status: In([
GameStatus.Downloading,
GameStatus.DownloadingMetadata,
GameStatus.CheckingFiles,
]),
},
{ status: GameStatus.Paused }
{ status: "active", progress: Not(1) },
{ status: "paused" }
);
if (game) {
@@ -62,19 +44,15 @@ const startGameDownload = async (
id: game.id,
},
{
status: GameStatus.DownloadingMetadata,
downloadPath: downloadPath,
status: "active",
progress: 0,
bytesDownloaded: 0,
downloadPath,
downloader,
repack: { id: repackId },
isDeleted: false,
}
);
DownloadManager.downloadGame(game.id);
game.status = GameStatus.DownloadingMetadata;
return game;
} else {
const steamGame = stateManager
.getValue("steamGames")
@@ -84,14 +62,14 @@ const startGameDownload = async (
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
: null;
const createdGame = await gameRepository
.save({
await gameRepository
.insert({
title,
iconUrl,
objectID,
downloader,
shop: gameShop,
status: GameStatus.Downloading,
shop,
status: "active",
downloadPath,
repack: { id: repackId },
})
@@ -104,13 +82,16 @@ const startGameDownload = async (
return result;
});
DownloadManager.downloadGame(createdGame.id);
const { repack: _, ...rest } = createdGame;
return rest;
}
const updatedGame = await gameRepository.findOne({
where: {
objectID,
},
relations: { repack: true },
});
await DownloadManager.startDownload(updatedGame!);
};
registerEvent("startGameDownload", startGameDownload);

View File

@@ -0,0 +1,14 @@
import { RealDebridClient } from "@main/services/real-debrid";
import { registerEvent } from "../register-event";
const authenticateRealDebrid = async (
_event: Electron.IpcMainInvokeEvent,
apiToken: string
) => {
RealDebridClient.authorize(apiToken);
const user = await RealDebridClient.getUser();
return user;
};
registerEvent("authenticateRealDebrid", authenticateRealDebrid);

View File

@@ -2,23 +2,17 @@ import { userPreferencesRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import type { UserPreferences } from "@types";
import { RealDebridClient } from "@main/services/real-debrid";
const updateUserPreferences = async (
_event: Electron.IpcMainInvokeEvent,
preferences: Partial<UserPreferences>
) => {
if (preferences.realDebridApiToken) {
RealDebridClient.authorize(preferences.realDebridApiToken);
}
await userPreferencesRepository.upsert(
) =>
userPreferencesRepository.upsert(
{
id: 1,
...preferences,
},
["id"]
);
};
registerEvent("updateUserPreferences", updateUserPreferences);

View File

@@ -85,5 +85,8 @@ export const steamUrlBuilder = {
`https://cdn.cloudflare.steamstatic.com/steam/apps/${objectID}/logo.png`,
};
export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
export * from "./formatters";
export * from "./ps";

View File

@@ -3,7 +3,12 @@ import updater from "electron-updater";
import i18n from "i18next";
import path from "node:path";
import { electronApp, optimizer } from "@electron-toolkit/utils";
import { logger, resolveDatabaseUpdates, WindowManager } from "@main/services";
import {
DownloadManager,
logger,
resolveDatabaseUpdates,
WindowManager,
} from "@main/services";
import { dataSource } from "@main/data-source";
import * as resources from "@locales";
import { userPreferencesRepository } from "@main/repository";
@@ -64,7 +69,7 @@ app.whenReady().then(() => {
where: { id: 1 },
});
WindowManager.createSplashScreen();
WindowManager.createMainWindow();
WindowManager.createSystemTray(userPreferences?.language || "en");
});
});
@@ -100,6 +105,10 @@ app.on("window-all-closed", () => {
WindowManager.mainWindow = null;
});
app.on("before-quit", () => {
DownloadManager.disconnect();
});
app.on("activate", () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.

View File

@@ -5,27 +5,25 @@ import {
getNewRepacksFromUser,
getNewRepacksFromXatab,
getNewRepacksFromOnlineFix,
startProcessWatcher,
DownloadManager,
startMainLoop,
} from "./services";
import {
gameRepository,
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();
startMainLoop();
const track1337xUsers = async (existingRepacks: Repack[]) => {
for (const repacker of repackersOn1337x) {
@@ -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,31 +80,24 @@ 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");
if (userPreferences?.realDebridApiToken)
await RealDebridClient.authorize(userPreferences?.realDebridApiToken);
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
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);
}
if (game) DownloadManager.startDownload(game);
};
userPreferencesRepository

View File

@@ -0,0 +1,49 @@
import { Game } from "@main/entity";
import { MigrationInterface, QueryRunner } from "typeorm";
export class AlterLastTimePlayedToDatime1716776027208
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
// 2024-05-27 02:08:17
// Mon, 27 May 2024 02:08:17 GMT
const updateLastTimePlayedValues = `
UPDATE game SET lastTimePlayed = (SELECT
SUBSTR(lastTimePlayed, 13, 4) || '-' || -- Year
CASE SUBSTR(lastTimePlayed, 9, 3)
WHEN 'Jan' THEN '01'
WHEN 'Feb' THEN '02'
WHEN 'Mar' THEN '03'
WHEN 'Apr' THEN '04'
WHEN 'May' THEN '05'
WHEN 'Jun' THEN '06'
WHEN 'Jul' THEN '07'
WHEN 'Aug' THEN '08'
WHEN 'Sep' THEN '09'
WHEN 'Oct' THEN '10'
WHEN 'Nov' THEN '11'
WHEN 'Dec' THEN '12'
END || '-' || -- Month
SUBSTR(lastTimePlayed, 6, 2) || ' ' || -- Day
SUBSTR(lastTimePlayed, 18, 8) -- hh:mm:ss;
FROM game)
WHERE lastTimePlayed IS NOT NULL;
`;
await queryRunner.query(updateLastTimePlayedValues);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const queryBuilder = queryRunner.manager.createQueryBuilder(Game, "game");
const result = await queryBuilder.getMany();
for (const game of result) {
if (!game.lastTimePlayed) continue;
await queryRunner.query(
`UPDATE game set lastTimePlayed = ? WHERE id = ?;`,
[game.lastTimePlayed.toUTCString(), game.id]
);
}
}
}

View File

@@ -1,3 +1,7 @@
import { FixRepackUploadDate1715900413313 } from "./1715900413313-fix_repack_uploadDate";
import { AlterLastTimePlayedToDatime1716776027208 } from "./1716776027208-alter_lastTimePlayed_to_datime";
export default [FixRepackUploadDate1715900413313];
export default [
FixRepackUploadDate1715900413313,
AlterLastTimePlayedToDatime1716776027208,
];

View File

@@ -0,0 +1,20 @@
import path from "node:path";
import { spawn } from "node:child_process";
import { app } from "electron";
export const startAria2 = () => {
const binaryPath = app.isPackaged
? path.join(process.resourcesPath, "aria2", "aria2c")
: path.join(__dirname, "..", "..", "aria2", "aria2c");
return spawn(
binaryPath,
[
"--enable-rpc",
"--rpc-listen-all",
"--file-allocation=none",
"--allow-overwrite=true",
],
{ stdio: "inherit", windowsHide: true }
);
};

View File

@@ -1,76 +1,303 @@
import { gameRepository } from "@main/repository";
import Aria2, { StatusResponse } from "aria2";
import type { Game } from "@main/entity";
import { gameRepository, userPreferencesRepository } from "@main/repository";
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";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { Game } from "@main/entity";
import { startAria2 } from "./aria2c";
import { sleep } from "@main/helpers";
import { logger } from "./logger";
import type { ChildProcess } from "node:child_process";
export class DownloadManager {
private static gameDownloading: Game;
private static downloads = new Map<number, string>();
static async getGame(gameId: number) {
return gameRepository.findOne({
where: { id: gameId, isDeleted: false },
relations: {
repack: true,
},
});
private static connected = false;
private static gid: string | null = null;
private static game: Game | null = null;
private static realDebridTorrentId: string | null = null;
private static aria2c: ChildProcess | null = null;
private static aria2 = new Aria2({});
private static async connect() {
this.aria2c = startAria2();
let retries = 0;
while (retries < 4 && !this.connected) {
try {
await this.aria2.open();
logger.log("Connected to aria2");
this.connected = true;
} catch (err) {
await sleep(100);
logger.log("Failed to connect to aria2, retrying...");
retries++;
}
}
}
static async cancelDownload() {
if (
this.gameDownloading &&
this.gameDownloading.downloader === Downloader.Torrent
) {
writePipe.write({ action: "cancel" });
} else {
RealDebridDownloader.destroy();
public static disconnect() {
if (this.aria2c) {
this.aria2c.kill();
}
}
private static getETA(
totalLength: number,
completedLength: number,
speed: number
) {
const remainingBytes = totalLength - completedLength;
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.game) {
new Notification({
title: t("download_complete", {
ns: "notifications",
lng: userPreferences.language,
}),
body: t("game_ready_to_install", {
ns: "notifications",
lng: userPreferences.language,
title: this.game.title,
}),
}).show();
}
}
private static getFolderName(status: StatusResponse) {
if (status.bittorrent?.info) return status.bittorrent.info.name;
return "";
}
private static async getRealDebridDownloadUrl() {
if (this.realDebridTorrentId) {
const torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId
);
const { status, links } = torrentInfo;
if (status === "waiting_files_selection") {
await RealDebridClient.selectAllFiles(this.realDebridTorrentId);
return null;
}
if (status === "downloaded") {
const [link] = links;
const { download } = await RealDebridClient.unrestrictLink(link);
return decodeURIComponent(download);
}
if (WindowManager.mainWindow) {
const progress = torrentInfo.progress / 100;
const totalDownloaded = progress * torrentInfo.bytes;
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
const payload = {
numPeers: 0,
numSeeds: torrentInfo.seeders,
downloadSpeed: torrentInfo.speed,
timeRemaining: this.getETA(
torrentInfo.bytes,
totalDownloaded,
torrentInfo.speed
),
isDownloadingMetadata: status === "magnet_conversion",
game: {
...this.game,
bytesDownloaded: progress * torrentInfo.bytes,
progress,
},
} as DownloadProgress;
WindowManager.mainWindow.webContents.send(
"on-download-progress",
JSON.parse(JSON.stringify(payload))
);
}
}
return null;
}
public static async watchDownloads() {
if (!this.game) return;
if (!this.gid && this.realDebridTorrentId) {
const options = { dir: this.game.downloadPath! };
const downloadUrl = await this.getRealDebridDownloadUrl();
if (downloadUrl) {
this.gid = await this.aria2.call("addUri", [downloadUrl], options);
this.downloads.set(this.game.id, this.gid);
this.realDebridTorrentId = null;
}
}
if (!this.gid) return;
const status = await this.aria2.call("tellStatus", this.gid);
const isDownloadingMetadata = status.bittorrent && !status.bittorrent?.info;
if (status.followedBy?.length) {
this.gid = status.followedBy[0];
this.downloads.set(this.game.id, this.gid);
return;
}
const progress =
Number(status.completedLength) / Number(status.totalLength);
if (!isDownloadingMetadata) {
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.game.id },
{
...update,
status: status.status,
folderName: this.getFolderName(status),
}
);
}
const game = await gameRepository.findOne({
where: { id: this.game.id, isDeleted: false },
relations: { repack: true },
});
if (progress === 1 && this.game && !isDownloadingMetadata) {
await this.publishNotification();
/*
Only cancel bittorrent downloads to stop seeding
*/
if (status.bittorrent) {
await this.cancelDownload(this.game.id);
} else {
this.clearCurrentDownload();
}
}
if (WindowManager.mainWindow && game) {
if (!isNaN(progress))
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
const payload = {
numPeers: Number(status.connections),
numSeeds: Number(status.numSeeders ?? 0),
downloadSpeed: Number(status.downloadSpeed),
timeRemaining: this.getETA(
Number(status.totalLength),
Number(status.completedLength),
Number(status.downloadSpeed)
),
isDownloadingMetadata: !!isDownloadingMetadata,
game,
} as DownloadProgress;
WindowManager.mainWindow.webContents.send(
"on-download-progress",
JSON.parse(JSON.stringify(payload))
);
}
}
private static clearCurrentDownload() {
if (this.game) {
this.downloads.delete(this.game.id);
this.gid = null;
this.game = null;
this.realDebridTorrentId = 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" });
if (this.gid) {
await this.aria2.call("forcePause", this.gid);
this.gid = null;
}
this.game = null;
this.realDebridTorrentId = null;
WindowManager.mainWindow?.setProgressBar(-1);
}
static async resumeDownload(game: Game) {
if (this.downloads.has(game.id)) {
const gid = this.downloads.get(game.id)!;
await this.aria2.call("unpause", gid);
this.gid = gid;
this.game = game;
this.realDebridTorrentId = null;
} else {
RealDebridDownloader.destroy();
return this.startDownload(game);
}
}
static async resumeDownload(gameId: number) {
const game = await this.getGame(gameId);
static async startDownload(game: Game) {
if (!this.connected) await this.connect();
if (game!.downloader === Downloader.Torrent) {
writePipe.write({
action: "start",
game_id: game!.id,
magnet: game!.repack.magnet,
save_path: game!.downloadPath,
});
const options = {
dir: game.downloadPath!,
};
if (game.downloader === Downloader.RealDebrid) {
this.realDebridTorrentId = await RealDebridClient.getTorrentId(
game!.repack.magnet
);
} else {
RealDebridDownloader.startDownload(game!);
this.gid = await this.aria2.call("addUri", [game.repack.magnet], options);
this.downloads.set(game.id, this.gid);
}
this.gameDownloading = game!;
}
static async downloadGame(gameId: number) {
const game = await this.getGame(gameId);
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!);
}
this.gameDownloading = game!;
this.game = 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,7 @@ 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";
export * from "./main-loop";

View File

@@ -1,11 +1,7 @@
import { app } from "electron";
import { logsPath } from "@main/constants";
import log from "electron-log";
import path from "path";
const logsPath = app.isPackaged
? path.join(app.getAppPath(), "..", "..", "logs")
: path.join(app.getAppPath(), "logs");
log.transports.file.resolvePathFn = (
_: log.PathVariables,
message?: log.LogMessage | undefined

View File

@@ -0,0 +1,15 @@
import { sleep } from "@main/helpers";
import { DownloadManager } from "./download-manager";
import { watchProcesses } from "./process-watcher";
export const startMainLoop = async () => {
// eslint-disable-next-line no-constant-condition
while (true) {
await Promise.allSettled([
watchProcesses(),
DownloadManager.watchDownloads(),
]);
await sleep(500);
}
};

View File

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

View File

@@ -1,15 +1,24 @@
import { Game } from "@main/entity";
import axios, { AxiosInstance } from "axios";
import parseTorrent from "parse-torrent";
import type {
RealDebridAddMagnet,
RealDebridTorrentInfo,
RealDebridUnrestrictLink,
} from "./real-debrid.types";
import axios, { AxiosInstance } from "axios";
const base = "https://api.real-debrid.com/rest/1.0";
RealDebridUser,
} from "@types";
export class RealDebridClient {
private static instance: AxiosInstance;
private static baseURL = "https://api.real-debrid.com/rest/1.0";
static authorize(apiToken: string) {
this.instance = axios.create({
baseURL: this.baseURL,
headers: {
Authorization: `Bearer ${apiToken}`,
},
});
}
static async addMagnet(magnet: string) {
const searchParams = new URLSearchParams({ magnet });
@@ -22,13 +31,18 @@ export class RealDebridClient {
return response.data;
}
static async getInfo(id: string) {
static async getTorrentInfo(id: string) {
const response = await this.instance.get<RealDebridTorrentInfo>(
`/torrents/info/${id}`
);
return response.data;
}
static async getUser() {
const response = await this.instance.get<RealDebridUser>(`/user`);
return response.data;
}
static async selectAllFiles(id: string) {
const searchParams = new URLSearchParams({ files: "all" });
@@ -49,51 +63,24 @@ export class RealDebridClient {
return response.data;
}
static async getAllTorrentsFromUser() {
private static async getAllTorrentsFromUser() {
const response =
await this.instance.get<RealDebridTorrentInfo[]>("/torrents");
return response.data;
}
static extractSHA1FromMagnet(magnet: string) {
return magnet.match(/btih:([0-9a-fA-F]*)/)?.[1].toLowerCase();
}
static async getTorrentId(magnetUri: string) {
const userTorrents = await RealDebridClient.getAllTorrentsFromUser();
static async getDownloadUrl(game: Game) {
const torrents = await RealDebridClient.getAllTorrentsFromUser();
const hash = RealDebridClient.extractSHA1FromMagnet(game!.repack.magnet);
let torrent = torrents.find((t) => t.hash === hash);
const { infoHash } = await parseTorrent(magnetUri);
const userTorrent = userTorrents.find(
(userTorrent) => userTorrent.hash === infoHash
);
if (!torrent) {
const magnet = await RealDebridClient.addMagnet(game!.repack.magnet);
if (userTorrent) return userTorrent.id;
if (magnet && magnet.id) {
await RealDebridClient.selectAllFiles(magnet.id);
torrent = await RealDebridClient.getInfo(magnet.id);
}
}
if (torrent) {
const { links } = torrent;
const { download } = await RealDebridClient.unrestrictLink(links[0]);
if (!download) {
throw new Error("Torrent not cached on Real Debrid");
}
return download;
}
throw new Error();
}
static async authorize(apiToken: string) {
this.instance = axios.create({
baseURL: base,
headers: {
Authorization: `Bearer ${apiToken}`,
},
});
const torrent = await RealDebridClient.addMagnet(magnetUri);
return torrent.id;
}
}

View File

@@ -1,51 +0,0 @@
export interface RealDebridUnrestrictLink {
id: string;
filename: string;
mimeType: string;
filesize: number;
link: string;
host: string;
host_icon: string;
chunks: number;
crc: number;
download: string;
streamable: number;
}
export interface RealDebridAddMagnet {
id: string;
// URL of the created ressource
uri: string;
}
export interface RealDebridTorrentInfo {
id: string;
filename: string;
original_filename: string; // Original name of the torrent
hash: string; // SHA1 Hash of the torrent
bytes: number; // Size of selected files only
original_bytes: number; // Total size of the torrent
host: string; // Host main domain
split: number; // Split size of links
progress: number; // Possible values: 0 to 100
status: string; // Current status of the torrent: magnet_error, magnet_conversion, waiting_files_selection, queued, downloading, downloaded, error, virus, compressing, uploading, dead
added: string; // jsonDate
files: [
{
id: number;
path: string; // Path to the file inside the torrent, starting with "/"
bytes: number;
selected: number; // 0 or 1
},
{
id: number;
path: string; // Path to the file inside the torrent, starting with "/"
bytes: number;
selected: number; // 0 or 1
},
];
links: string[];
ended: string; // !! Only present when finished, jsonDate
speed: number; // !! Only present in "downloading", "compressing", "uploading" status
seeders: number; // !! Only present in "downloading", "magnet_conversion" status
}

View File

@@ -17,8 +17,6 @@ import { IsNull, Not } from "typeorm";
export class WindowManager {
public static mainWindow: Electron.BrowserWindow | null = null;
public static splashWindow: Electron.BrowserWindow | null = null;
public static isReadyToShowMainWindow = false;
private static loadURL(hash = "") {
// HMR for renderer base on electron-vite cli.
@@ -37,44 +35,8 @@ export class WindowManager {
}
}
private static loadSplashURL() {
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
this.splashWindow?.loadURL(
`${process.env["ELECTRON_RENDERER_URL"]}#/splash`
);
} else {
this.splashWindow?.loadFile(
path.join(__dirname, "../renderer/index.html"),
{
hash: "splash",
}
);
}
}
public static createSplashScreen() {
if (this.splashWindow) return;
this.splashWindow = new BrowserWindow({
width: 380,
height: 380,
frame: false,
resizable: false,
backgroundColor: "#1c1c1c",
webPreferences: {
preload: path.join(__dirname, "../preload/index.mjs"),
sandbox: false,
},
});
this.loadSplashURL();
this.splashWindow.removeMenu();
}
public static createMainWindow() {
if (this.mainWindow || !this.isReadyToShowMainWindow) return;
if (this.mainWindow) return;
this.mainWindow = new BrowserWindow({
width: 1200,
@@ -94,6 +56,7 @@ export class WindowManager {
preload: path.join(__dirname, "../preload/index.mjs"),
sandbox: false,
},
show: false,
});
this.loadURL();
@@ -101,6 +64,7 @@ export class WindowManager {
this.mainWindow.on("ready-to-show", () => {
if (!app.isPackaged) WindowManager.mainWindow?.webContents.openDevTools();
WindowManager.mainWindow?.show();
});
this.mainWindow.on("close", async () => {
@@ -115,12 +79,6 @@ export class WindowManager {
});
}
public static prepareMainWindowAndCloseSplash() {
this.isReadyToShowMainWindow = true;
this.splashWindow?.close();
this.createMainWindow();
}
public static redirect(hash: string) {
if (!this.mainWindow) this.createMainWindow();
this.loadURL(hash);
@@ -141,7 +99,7 @@ export class WindowManager {
},
take: 5,
order: {
updatedAt: "DESC",
lastTimePlayed: "DESC",
},
});