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;
export const isReady = (status: GameStatus | null) =>
status === GameStatus.Finished ||
status === GameStatus.Seeding;
}
status === GameStatus.Finished || status === GameStatus.Seeding;
}

View File

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

View File

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

View File

@ -45,7 +45,7 @@ const cancelGameDownload = async (
game.status !== GameStatus.Seeding
) {
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 { 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
// 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");
};
loadState().then(() => checkForNewRepacks());
loadState().then(() => checkForNewRepacks());

View File

@ -10,139 +10,168 @@ import { TorrentUpdate } from "./torrent-client";
import { HTTPDownloader } from "./http-downloader";
import { Unrar } from "../unrar";
import { GameStatus } from "@globals";
import path from 'node:path';
import path from "node:path";
interface DownloadStatus {
numPeers: number;
numSeeds: number;
downloadSpeed: number;
timeRemaining: number;
numPeers: number;
numSeeds: number;
downloadSpeed: number;
timeRemaining: number;
}
export class Downloader {
private static lastHttpDownloader: HTTPDownloader | null = null;
private static lastHttpDownloader: HTTPDownloader | null = null;
static async usesRealDebrid() {
const userPreferences = await userPreferencesRepository.findOne({ where: { id: 1 } });
return userPreferences!.realDebridApiToken !== null;
static async usesRealDebrid() {
const userPreferences = await userPreferencesRepository.findOne({
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 (!await this.usesRealDebrid()) {
writePipe.write({ action: "cancel" });
} else {
if (this.lastHttpDownloader) {
this.lastHttpDownloader.cancel();
}
}
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
);
}
static async pauseDownload() {
if (!await this.usesRealDebrid()) {
writePipe.write({ action: "pause" });
} else {
if (this.lastHttpDownloader) {
this.lastHttpDownloader.pause();
}
}
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 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();
}
}
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;
}
}
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 { ElectronDownloadManager } from 'electron-dl-manager';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { WindowManager } from '../window-manager';
import { Downloader } from './downloader';
import { GameStatus } from '@globals';
import { Game } from "@main/entity";
import { ElectronDownloadManager } from "electron-dl-manager";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { WindowManager } from "../window-manager";
import { Downloader } from "./downloader";
import { GameStatus } from "@globals";
function dropExtension(fileName: string) {
return fileName.split('.').slice(0, -1).join('.');
return fileName.split(".").slice(0, -1).join(".");
}
export class HTTPDownloader {
private downloadManager: ElectronDownloadManager;
private downloadId: string | null = null;
constructor() {
this.downloadManager = new ElectronDownloadManager();
}
async download(url: string, destination: string, gameId: number) {
const window = WindowManager.mainWindow;
private downloadManager: ElectronDownloadManager;
private downloadId: string | null = null;
this.downloadId = await this.downloadManager.download({
url,
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/`,
});
}
constructor() {
this.downloadManager = new ElectronDownloadManager();
}
pause() {
if (this.downloadId) {
this.downloadManager.pauseDownload(this.downloadId);
}
}
async download(url: string, destination: string, gameId: number) {
const window = WindowManager.mainWindow;
cancel() {
if (this.downloadId) {
this.downloadManager.cancelDownload(this.downloadId);
}
}
this.downloadId = await this.downloadManager.download({
url,
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() {
if (this.downloadId) {
this.downloadManager.resumeDownload(this.downloadId);
}
pause() {
if (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 {
id: string;
filename: string;
mimeType: string;
filesize: number;
link: string;
host: string;
host_icon: string;
chunks: number;
crc: number;
download: string;
streamable: number;
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
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": "downloaded", // 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" // Host URL
],
"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
}
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: "downloaded"; // 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", // Host URL
];
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

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

View File

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

View File

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

View File

@ -118,7 +118,8 @@ export function Sidebar() {
}, [isResizing]);
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) {
const isVerifying = GameStatus.isVerifying(gameDownloading.status);

View File

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

View File

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