refactor: prettier changes

This commit is contained in:
lilezek 2024-04-30 11:24:35 +02:00
parent a16a30761e
commit e79b6f1391
16 changed files with 380 additions and 332 deletions

View file

@ -21,6 +21,5 @@ export namespace GameStatus {
GameStatus.Decompressing == status; GameStatus.Decompressing == status;
export const isReady = (status: GameStatus | null) => export const isReady = (status: GameStatus | null) =>
status === GameStatus.Finished || status === GameStatus.Finished || status === GameStatus.Seeding;
status === GameStatus.Seeding; }
}

View file

@ -46,9 +46,9 @@ export class Game {
@Column("text", { nullable: true }) @Column("text", { nullable: true })
status: GameStatus | null; status: GameStatus | null;
/** /**
* Progress is a float between 0 and 1 * Progress is a float between 0 and 1
*/ */
@Column("float", { default: 0 }) @Column("float", { default: 0 })
progress: number; progress: number;

View file

@ -35,4 +35,3 @@ export class UserPreferences {
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; updatedAt: Date;
} }

View file

@ -45,7 +45,7 @@ const cancelGameDownload = async (
game.status !== GameStatus.Seeding game.status !== GameStatus.Seeding
) { ) {
Downloader.cancelDownload(); Downloader.cancelDownload();
if (result.affected) WindowManager.mainWindow.setProgressBar(-1); if (result.affected) WindowManager.mainWindow?.setProgressBar(-1);
} }
}); });
}; };

View file

@ -1,4 +1,4 @@
import { getSteamGameIconUrl, writePipe } from "@main/services"; import { getSteamGameIconUrl } from "@main/services";
import { gameRepository, repackRepository } from "@main/repository"; import { gameRepository, repackRepository } from "@main/repository";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";

View file

@ -106,4 +106,4 @@ app.on("activate", () => {
}); });
// In this file you can include the rest of your app's specific main process // In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here. // code. You can also put them in separate files and import them here.

View file

@ -123,4 +123,4 @@ const loadState = async () => {
import("./events"); import("./events");
}; };
loadState().then(() => checkForNewRepacks()); loadState().then(() => checkForNewRepacks());

View file

@ -10,139 +10,168 @@ import { TorrentUpdate } from "./torrent-client";
import { HTTPDownloader } from "./http-downloader"; import { HTTPDownloader } from "./http-downloader";
import { Unrar } from "../unrar"; import { Unrar } from "../unrar";
import { GameStatus } from "@globals"; import { GameStatus } from "@globals";
import path from 'node:path'; import path from "node:path";
interface DownloadStatus { interface DownloadStatus {
numPeers: number; numPeers: number;
numSeeds: number; numSeeds: number;
downloadSpeed: number; downloadSpeed: number;
timeRemaining: number; timeRemaining: number;
} }
export class Downloader { export class Downloader {
private static lastHttpDownloader: HTTPDownloader | null = null; private static lastHttpDownloader: HTTPDownloader | null = null;
static async usesRealDebrid() { static async usesRealDebrid() {
const userPreferences = await userPreferencesRepository.findOne({ where: { id: 1 } }); const userPreferences = await userPreferencesRepository.findOne({
return userPreferences!.realDebridApiToken !== null; where: { id: 1 },
});
return userPreferences!.realDebridApiToken !== null;
}
static async cancelDownload() {
if (!(await this.usesRealDebrid())) {
writePipe.write({ action: "cancel" });
} else {
if (this.lastHttpDownloader) {
this.lastHttpDownloader.cancel();
}
}
}
static async pauseDownload() {
if (!(await this.usesRealDebrid())) {
writePipe.write({ action: "pause" });
} else {
if (this.lastHttpDownloader) {
this.lastHttpDownloader.pause();
}
}
}
static async resumeDownload() {
if (!(await this.usesRealDebrid())) {
writePipe.write({ action: "pause" });
} else {
if (this.lastHttpDownloader) {
this.lastHttpDownloader.resume();
}
}
}
static async downloadGame(game: Game, repack: Repack) {
if (!(await this.usesRealDebrid())) {
writePipe.write({
action: "start",
game_id: game.id,
magnet: repack.magnet,
save_path: game.downloadPath,
});
} else {
try {
const torrent = await RealDebridClient.addMagnet(repack.magnet);
if (torrent && torrent.id) {
await RealDebridClient.selectAllFiles(torrent.id);
const { links } = await RealDebridClient.getInfo(torrent.id);
const { download } = await RealDebridClient.unrestrictLink(links[0]);
this.lastHttpDownloader = new HTTPDownloader();
this.lastHttpDownloader.download(
download,
game.downloadPath!,
game.id
);
}
} catch (e) {
console.error(e);
}
}
}
static async updateGameProgress(
gameId: number,
gameUpdate: QueryDeepPartialEntity<Game>,
downloadStatus: DownloadStatus
) {
await gameRepository.update({ id: gameId }, gameUpdate);
const game = await gameRepository.findOne({
where: { id: gameId },
relations: { repack: true },
});
if (
gameUpdate.progress === 1 &&
gameUpdate.status !== GameStatus.Decompressing
) {
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();
}
} }
static async cancelDownload() { if (
if (!await this.usesRealDebrid()) { game &&
writePipe.write({ action: "cancel" }); gameUpdate.decompressionProgress === 0 &&
} else { gameUpdate.status === GameStatus.Decompressing
if (this.lastHttpDownloader) { ) {
this.lastHttpDownloader.cancel(); const unrar = await Unrar.fromFilePath(
} game.rarPath!,
} path.join(game.downloadPath!, game.folderName!)
);
unrar.extract();
this.updateGameProgress(
gameId,
{
decompressionProgress: 1,
status: GameStatus.Finished,
},
downloadStatus
);
} }
static async pauseDownload() { if (WindowManager.mainWindow && game) {
if (!await this.usesRealDebrid()) { const progress = this.getGameProgress(game);
writePipe.write({ action: "pause" }); WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
} else {
if (this.lastHttpDownloader) { WindowManager.mainWindow.webContents.send(
this.lastHttpDownloader.pause(); "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,
})
)
);
} }
}
static async resumeDownload() { static getGameProgress(game: Game) {
if (!await this.usesRealDebrid()) { if (game.status === GameStatus.CheckingFiles)
writePipe.write({ action: "pause" }); return game.fileVerificationProgress;
} else { if (game.status === GameStatus.Decompressing)
if (this.lastHttpDownloader) { return game.decompressionProgress;
this.lastHttpDownloader.resume(); return game.progress;
} }
} }
}
static async downloadGame(game: Game, repack: Repack) {
if (!await this.usesRealDebrid()) {
writePipe.write({
action: "start",
game_id: game.id,
magnet: repack.magnet,
save_path: game.downloadPath,
});
} else {
try {
const torrent = await RealDebridClient.addMagnet(repack.magnet);
if (torrent && torrent.id) {
await RealDebridClient.selectAllFiles(torrent.id);
const { links } = await RealDebridClient.getInfo(torrent.id);
const { download } = await RealDebridClient.unrestrictLink(links[0]);
this.lastHttpDownloader = new HTTPDownloader();
this.lastHttpDownloader.download(download, game.downloadPath!, game.id);
}
} catch (e) {
console.error(e);
}
}
}
static async updateGameProgress(gameId: number, gameUpdate: QueryDeepPartialEntity<Game>, downloadStatus: DownloadStatus) {
await gameRepository.update({ id: gameId }, gameUpdate);
const game = await gameRepository.findOne({
where: { id: gameId },
relations: { repack: true },
});
if (gameUpdate.progress === 1 && gameUpdate.status !== GameStatus.Decompressing) {
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 (game && gameUpdate.decompressionProgress === 0 && gameUpdate.status === GameStatus.Decompressing) {
const unrar = await Unrar.fromFilePath(game.rarPath!, path.join(game.downloadPath!, game.folderName!));
unrar.extract();
this.updateGameProgress(gameId, {
decompressionProgress: 1,
status: GameStatus.Finished,
}, downloadStatus);
}
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
}))
);
}
}
static getGameProgress(game: Game) {
if (game.status === GameStatus.CheckingFiles) return game.fileVerificationProgress;
if (game.status === GameStatus.Decompressing) return game.decompressionProgress;
return game.progress;
}
}

View file

@ -1,94 +1,106 @@
import { Game } from '@main/entity'; import { Game } from "@main/entity";
import { ElectronDownloadManager } from 'electron-dl-manager'; import { ElectronDownloadManager } from "electron-dl-manager";
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { WindowManager } from '../window-manager'; import { WindowManager } from "../window-manager";
import { Downloader } from './downloader'; import { Downloader } from "./downloader";
import { GameStatus } from '@globals'; import { GameStatus } from "@globals";
function dropExtension(fileName: string) { function dropExtension(fileName: string) {
return fileName.split('.').slice(0, -1).join('.'); return fileName.split(".").slice(0, -1).join(".");
} }
export class HTTPDownloader { export class HTTPDownloader {
private downloadManager: ElectronDownloadManager; private downloadManager: ElectronDownloadManager;
private downloadId: string | null = null; private downloadId: string | null = null;
constructor() {
this.downloadManager = new ElectronDownloadManager();
}
async download(url: string, destination: string, gameId: number) {
const window = WindowManager.mainWindow;
this.downloadId = await this.downloadManager.download({ constructor() {
url, this.downloadManager = new ElectronDownloadManager();
window: window!, }
callbacks: {
onDownloadStarted: async (ev) => {
const updatePayload: QueryDeepPartialEntity<Game> = {
status: GameStatus.Downloading,
progress: 0,
bytesDownloaded: 0,
fileSize: ev.item.getTotalBytes(),
rarPath: `${destination}/.rd/${ev.resolvedFilename}`,
folderName: dropExtension(ev.resolvedFilename)
};
const downloadStatus = {
numPeers: 0,
numSeeds: 0,
downloadSpeed: 0,
timeRemaining: Number.POSITIVE_INFINITY,
};
await Downloader.updateGameProgress(gameId, updatePayload, downloadStatus);
},
onDownloadCompleted: async (ev) => {
const updatePayload: QueryDeepPartialEntity<Game> = {
progress: 1,
decompressionProgress: 0,
bytesDownloaded: ev.item.getReceivedBytes(),
status: GameStatus.Decompressing,
};
const downloadStatus = {
numPeers: 1,
numSeeds: 1,
downloadSpeed: 0,
timeRemaining: 0,
};
await Downloader.updateGameProgress(gameId, updatePayload, downloadStatus);
},
onDownloadProgress: async (ev) => {
const updatePayload: QueryDeepPartialEntity<Game> = {
progress: ev.percentCompleted / 100,
bytesDownloaded: ev.item.getReceivedBytes(),
};
const downloadStatus = {
numPeers: 1,
numSeeds: 1,
downloadSpeed: ev.downloadRateBytesPerSecond,
timeRemaining: ev.estimatedTimeRemainingSeconds,
};
await Downloader.updateGameProgress(gameId, updatePayload, downloadStatus);
}
},
directory: `${destination}/.rd/`,
});
}
pause() { async download(url: string, destination: string, gameId: number) {
if (this.downloadId) { const window = WindowManager.mainWindow;
this.downloadManager.pauseDownload(this.downloadId);
}
}
cancel() { this.downloadId = await this.downloadManager.download({
if (this.downloadId) { url,
this.downloadManager.cancelDownload(this.downloadId); window: window!,
} callbacks: {
} onDownloadStarted: async (ev) => {
const updatePayload: QueryDeepPartialEntity<Game> = {
status: GameStatus.Downloading,
progress: 0,
bytesDownloaded: 0,
fileSize: ev.item.getTotalBytes(),
rarPath: `${destination}/.rd/${ev.resolvedFilename}`,
folderName: dropExtension(ev.resolvedFilename),
};
const downloadStatus = {
numPeers: 0,
numSeeds: 0,
downloadSpeed: 0,
timeRemaining: Number.POSITIVE_INFINITY,
};
await Downloader.updateGameProgress(
gameId,
updatePayload,
downloadStatus
);
},
onDownloadCompleted: async (ev) => {
const updatePayload: QueryDeepPartialEntity<Game> = {
progress: 1,
decompressionProgress: 0,
bytesDownloaded: ev.item.getReceivedBytes(),
status: GameStatus.Decompressing,
};
const downloadStatus = {
numPeers: 1,
numSeeds: 1,
downloadSpeed: 0,
timeRemaining: 0,
};
await Downloader.updateGameProgress(
gameId,
updatePayload,
downloadStatus
);
},
onDownloadProgress: async (ev) => {
const updatePayload: QueryDeepPartialEntity<Game> = {
progress: ev.percentCompleted / 100,
bytesDownloaded: ev.item.getReceivedBytes(),
};
const downloadStatus = {
numPeers: 1,
numSeeds: 1,
downloadSpeed: ev.downloadRateBytesPerSecond,
timeRemaining: ev.estimatedTimeRemainingSeconds,
};
await Downloader.updateGameProgress(
gameId,
updatePayload,
downloadStatus
);
},
},
directory: `${destination}/.rd/`,
});
}
resume() { pause() {
if (this.downloadId) { if (this.downloadId) {
this.downloadManager.resumeDownload(this.downloadId); this.downloadManager.pauseDownload(this.downloadId);
}
} }
} }
cancel() {
if (this.downloadId) {
this.downloadManager.cancelDownload(this.downloadId);
}
}
resume() {
if (this.downloadId) {
this.downloadManager.resumeDownload(this.downloadId);
}
}
}

View file

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

View file

@ -1,55 +1,61 @@
import { userPreferencesRepository } from "@main/repository"; import { userPreferencesRepository } from "@main/repository";
import fetch from "node-fetch"; import fetch from "node-fetch";
import { RealDebridAddMagnet, RealDebridTorrentInfo, RealDebridUnrestrictLink } from "./real-debrid-types"; import {
RealDebridAddMagnet,
RealDebridTorrentInfo,
RealDebridUnrestrictLink,
} from "./real-debrid-types";
const base = "https://api.real-debrid.com/rest/1.0"; const base = "https://api.real-debrid.com/rest/1.0";
export class RealDebridClient { export class RealDebridClient {
static async addMagnet(magnet: string) { static async addMagnet(magnet: string) {
const response = await fetch(`${base}/torrents/addMagnet`, { const response = await fetch(`${base}/torrents/addMagnet`, {
method: "POST", method: "POST",
headers: { headers: {
"Authorization": `Bearer ${await this.getApiToken()}`, Authorization: `Bearer ${await this.getApiToken()}`,
}, },
body: `magnet=${encodeURIComponent(magnet)}` body: `magnet=${encodeURIComponent(magnet)}`,
}); });
return response.json() as Promise<RealDebridAddMagnet>; return response.json() as Promise<RealDebridAddMagnet>;
} }
static async getInfo(id: string) { static async getInfo(id: string) {
const response = await fetch(`${base}/torrents/info/${id}`, { const response = await fetch(`${base}/torrents/info/${id}`, {
headers: { headers: {
"Authorization": `Bearer ${await this.getApiToken()}` Authorization: `Bearer ${await this.getApiToken()}`,
} },
}); });
return response.json() as Promise<RealDebridTorrentInfo>; return response.json() as Promise<RealDebridTorrentInfo>;
} }
static async selectAllFiles(id: string) { static async selectAllFiles(id: string) {
const response = await fetch(`${base}/torrents/selectFiles/${id}`, { await fetch(`${base}/torrents/selectFiles/${id}`, {
method: "POST", method: "POST",
headers: { headers: {
"Authorization": `Bearer ${await this.getApiToken()}`, Authorization: `Bearer ${await this.getApiToken()}`,
}, },
body: "files=all" body: "files=all",
}); });
} }
static async unrestrictLink(link: string) { static async unrestrictLink(link: string) {
const response = await fetch(`${base}/unrestrict/link`, { const response = await fetch(`${base}/unrestrict/link`, {
method: "POST", method: "POST",
headers: { headers: {
"Authorization": `Bearer ${await this.getApiToken()}`, Authorization: `Bearer ${await this.getApiToken()}`,
}, },
body: `link=${link}` body: `link=${link}`,
}); });
return response.json() as Promise<RealDebridUnrestrictLink>; return response.json() as Promise<RealDebridUnrestrictLink>;
} }
static getApiToken() { static getApiToken() {
return userPreferencesRepository.findOne({ where: { id: 1 } }).then(userPreferences => userPreferences!.realDebridApiToken); return userPreferencesRepository
} .findOne({ where: { id: 1 } })
} .then((userPreferences) => userPreferences!.realDebridApiToken);
}
}

View file

@ -131,4 +131,4 @@ export class TorrentClient {
Sentry.captureException(err); Sentry.captureException(err);
} }
} }
} }

View file

@ -1,24 +1,26 @@
import { Extractor, createExtractorFromFile } from 'node-unrar-js'; import { Extractor, createExtractorFromFile } from "node-unrar-js";
import fs from 'node:fs'; import fs from "node:fs";
const wasmBinary = fs.readFileSync(require.resolve('node-unrar-js/esm/js/unrar.wasm')); const wasmBinary = fs.readFileSync(
require.resolve("node-unrar-js/esm/js/unrar.wasm")
);
export class Unrar { export class Unrar {
private constructor(private extractor: Extractor<Uint8Array>) { } private constructor(private extractor: Extractor<Uint8Array>) {}
static async fromFilePath(filePath: string, targetFolder: string) { static async fromFilePath(filePath: string, targetFolder: string) {
const extractor = await createExtractorFromFile({ const extractor = await createExtractorFromFile({
filepath: filePath, filepath: filePath,
targetPath: targetFolder, targetPath: targetFolder,
wasmBinary, wasmBinary,
}); });
return new Unrar(extractor); return new Unrar(extractor);
} }
extract() { extract() {
const files = this.extractor.extract().files; const files = this.extractor.extract().files;
for (const file of files) { for (const file of files) {
console.log("File:", file.fileHeader.name); console.log("File:", file.fileHeader.name);
}
} }
}
} }

View file

@ -118,7 +118,8 @@ export function Sidebar() {
}, [isResizing]); }, [isResizing]);
const getGameTitle = (game: Game) => { const getGameTitle = (game: Game) => {
if (game.status === GameStatus.Paused) return t("paused", { title: game.title }); if (game.status === GameStatus.Paused)
return t("paused", { title: game.title });
if (gameDownloading?.id === game.id) { if (gameDownloading?.id === game.id) {
const isVerifying = GameStatus.isVerifying(gameDownloading.status); const isVerifying = GameStatus.isVerifying(gameDownloading.status);

View file

@ -99,7 +99,7 @@ export function useDownload() {
dispatch(setGameDeleting(gameId)); dispatch(setGameDeleting(gameId));
return window.electron.deleteGameFolder(gameId); return window.electron.deleteGameFolder(gameId);
}) })
.catch(() => { }) .catch(() => {})
.finally(() => { .finally(() => {
updateLibrary(); updateLibrary();
dispatch(removeGameFromDeleting(gameId)); dispatch(removeGameFromDeleting(gameId));

View file

@ -111,12 +111,12 @@ export function Settings() {
/> />
<TextField <TextField
label={t("real_debrid_api_token")} label={t("real_debrid_api_token")}
value={form.realDebridApiToken ?? ""} value={form.realDebridApiToken ?? ""}
onChange={(event) => { onChange={(event) => {
updateUserPreferences("realDebridApiToken", event.target.value); updateUserPreferences("realDebridApiToken", event.target.value);
}} }}
/> />
</div> </div>
</section> </section>
); );