diff --git a/.gitignore b/.gitignore
index 7a6496a5..fb4badd7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
.vscode
node_modules
hydra-download-manager/
-aria2/
fastlist.exe
__pycache__
dist
diff --git a/electron-builder.yml b/electron-builder.yml
index be300d36..cfdafe7d 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -3,7 +3,6 @@ productName: Hydra
directories:
buildResources: build
extraResources:
- - aria2
- hydra-download-manager
- seeds
- from: node_modules/create-desktop-shortcuts/src/windows.vbs
diff --git a/package.json b/package.json
index 1b99734d..aa77084e 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
- "postinstall": "electron-builder install-app-deps && node ./postinstall.cjs",
+ "postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "electron-vite build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
@@ -34,15 +34,13 @@
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
- "@fontsource/fira-mono": "^5.0.13",
- "@fontsource/fira-sans": "^5.0.20",
+ "@fontsource/noto-sans": "^5.0.22",
"@primer/octicons-react": "^19.9.0",
"@reduxjs/toolkit": "^2.2.3",
"@sentry/electron": "^5.1.0",
"@vanilla-extract/css": "^1.14.2",
"@vanilla-extract/dynamic": "^2.1.1",
"@vanilla-extract/recipes": "^0.5.2",
- "aria2": "^4.1.2",
"auto-launch": "^5.0.6",
"axios": "^1.6.8",
"better-sqlite3": "^9.5.0",
@@ -97,7 +95,7 @@
"@types/user-agents": "^1.0.4",
"@vanilla-extract/vite-plugin": "^4.0.7",
"@vitejs/plugin-react": "^4.2.1",
- "electron": "^30.0.9",
+ "electron": "^30.3.0",
"electron-builder": "^24.9.1",
"electron-vite": "^2.0.0",
"eslint": "^8.56.0",
diff --git a/postinstall.cjs b/postinstall.cjs
deleted file mode 100644
index 547af988..00000000
--- a/postinstall.cjs
+++ /dev/null
@@ -1,50 +0,0 @@
-const { default: axios } = require("axios");
-const util = require("node:util");
-const fs = require("node:fs");
-
-const exec = util.promisify(require("node:child_process").exec);
-
-const downloadAria2 = async () => {
- if (fs.existsSync("aria2")) {
- console.log("Aria2 already exists, skipping download...");
- return;
- }
-
- const file =
- process.platform === "win32"
- ? "aria2-1.37.0-win-64bit-build1.zip"
- : "aria2-1.37.0-1-x86_64.pkg.tar.zst";
-
- const downloadUrl =
- process.platform === "win32"
- ? `https://github.com/aria2/aria2/releases/download/release-1.37.0/${file}`
- : "https://archlinux.org/packages/extra/x86_64/aria2/download/";
-
- console.log(`Downloading ${file}...`);
-
- const response = await axios.get(downloadUrl, { responseType: "stream" });
-
- const stream = response.data.pipe(fs.createWriteStream(file));
-
- stream.on("finish", async () => {
- console.log(`Downloaded ${file}, extracting...`);
-
- if (process.platform === "win32") {
- await exec(`npx extract-zip ${file}`);
- console.log("Extracted. Renaming folder...");
-
- fs.renameSync(file.replace(".zip", ""), "aria2");
- } else {
- await exec(`tar --zstd -xvf ${file} usr/bin/aria2c`);
- console.log("Extracted. Copying binary file...");
- fs.mkdirSync("aria2");
- fs.copyFileSync("usr/bin/aria2c", "aria2/aria2c");
- fs.rmSync("usr", { recursive: true });
- }
-
- console.log(`Extracted ${file}, removing compressed downloaded file...`);
- fs.rmSync(file);
- });
-};
-
-downloadAria2();
diff --git a/src/main/declaration.d.ts b/src/main/declaration.d.ts
deleted file mode 100644
index ac2675a3..00000000
--- a/src/main/declaration.d.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-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;
- call(
- method: "addUri",
- uris: string[],
- options: { dir: string }
- ): Promise;
- call(
- method: "tellStatus",
- gid: string,
- keys?: string[]
- ): Promise;
- call(method: "pause", gid: string): Promise;
- call(method: "forcePause", gid: string): Promise;
- call(method: "unpause", gid: string): Promise;
- call(method: "remove", gid: string): Promise;
- call(method: "forceRemove", gid: string): Promise;
- call(method: "pauseAll"): Promise;
- call(method: "forcePauseAll"): Promise;
- listNotifications: () => [
- "onDownloadStart",
- "onDownloadPause",
- "onDownloadStop",
- "onDownloadComplete",
- "onDownloadError",
- "onBtDownloadComplete",
- ];
- on: (event: string, callback: (params: any) => void) => void;
- }
-}
diff --git a/src/main/events/auth/sign-out.ts b/src/main/events/auth/sign-out.ts
index fe640b9d..9998c733 100644
--- a/src/main/events/auth/sign-out.ts
+++ b/src/main/events/auth/sign-out.ts
@@ -26,6 +26,8 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
/* Disconnects libtorrent */
PythonInstance.killTorrent();
+ HydraApi.handleSignOut();
+
await Promise.all([
databaseOperations,
HydraApi.post("/auth/logout").catch(() => {}),
diff --git a/src/main/main.ts b/src/main/main.ts
index fbabc56c..af594e20 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -22,8 +22,9 @@ const loadState = async (userPreferences: UserPreferences | null) => {
import("./events");
- if (userPreferences?.realDebridApiToken)
+ if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
+ }
HydraApi.setupApi().then(() => {
uploadGamesBatch();
diff --git a/src/main/services/aria2c.ts b/src/main/services/aria2c.ts
deleted file mode 100644
index b1b1da76..00000000
--- a/src/main/services/aria2c.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-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 }
- );
-};
diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts
index 31f28992..52a66693 100644
--- a/src/main/services/download/download-manager.ts
+++ b/src/main/services/download/download-manager.ts
@@ -6,6 +6,8 @@ import { downloadQueueRepository, gameRepository } from "@main/repository";
import { publishDownloadCompleteNotification } from "../notifications";
import { RealDebridDownloader } from "./real-debrid-downloader";
import type { DownloadProgress } from "@types";
+import { GofileApi } from "../hosters";
+import { GenericHTTPDownloader } from "./generic-http-downloader";
export class DownloadManager {
private static currentDownloader: Downloader | null = null;
@@ -13,10 +15,12 @@ export class DownloadManager {
public static async watchDownloads() {
let status: DownloadProgress | null = null;
- if (this.currentDownloader === Downloader.RealDebrid) {
+ if (this.currentDownloader === Downloader.Torrent) {
+ status = await PythonInstance.getStatus();
+ } else if (this.currentDownloader === Downloader.RealDebrid) {
status = await RealDebridDownloader.getStatus();
} else {
- status = await PythonInstance.getStatus();
+ status = await GenericHTTPDownloader.getStatus();
}
if (status) {
@@ -62,10 +66,12 @@ export class DownloadManager {
}
static async pauseDownload() {
- if (this.currentDownloader === Downloader.RealDebrid) {
+ if (this.currentDownloader === Downloader.Torrent) {
+ await PythonInstance.pauseDownload();
+ } else if (this.currentDownloader === Downloader.RealDebrid) {
await RealDebridDownloader.pauseDownload();
} else {
- await PythonInstance.pauseDownload();
+ await GenericHTTPDownloader.pauseDownload();
}
WindowManager.mainWindow?.setProgressBar(-1);
@@ -73,20 +79,16 @@ export class DownloadManager {
}
static async resumeDownload(game: Game) {
- if (game.downloader === Downloader.RealDebrid) {
- RealDebridDownloader.startDownload(game);
- this.currentDownloader = Downloader.RealDebrid;
- } else {
- PythonInstance.startDownload(game);
- this.currentDownloader = Downloader.Torrent;
- }
+ return this.startDownload(game);
}
static async cancelDownload(gameId: number) {
- if (this.currentDownloader === Downloader.RealDebrid) {
+ if (this.currentDownloader === Downloader.Torrent) {
+ PythonInstance.cancelDownload(gameId);
+ } else if (this.currentDownloader === Downloader.RealDebrid) {
RealDebridDownloader.cancelDownload(gameId);
} else {
- PythonInstance.cancelDownload(gameId);
+ GenericHTTPDownloader.cancelDownload(gameId);
}
WindowManager.mainWindow?.setProgressBar(-1);
@@ -94,12 +96,28 @@ export class DownloadManager {
}
static async startDownload(game: Game) {
- if (game.downloader === Downloader.RealDebrid) {
- RealDebridDownloader.startDownload(game);
- this.currentDownloader = Downloader.RealDebrid;
- } else {
+ if (game.downloader === Downloader.Gofile) {
+ const id = game!.uri!.split("/").pop();
+
+ const token = await GofileApi.authorize();
+ const downloadLink = await GofileApi.getDownloadLink(id!);
+
+ GenericHTTPDownloader.startDownload(game, downloadLink, {
+ Cookie: `accountToken=${token}`,
+ });
+ } else if (game.downloader === Downloader.PixelDrain) {
+ const id = game!.uri!.split("/").pop();
+
+ await GenericHTTPDownloader.startDownload(
+ game,
+ `https://pixeldrain.com/api/file/${id}?download`
+ );
+ } else if (game.downloader === Downloader.Torrent) {
PythonInstance.startDownload(game);
- this.currentDownloader = Downloader.Torrent;
+ } else if (game.downloader === Downloader.RealDebrid) {
+ RealDebridDownloader.startDownload(game);
}
+
+ this.currentDownloader = game.downloader;
}
}
diff --git a/src/main/services/download/generic-http-downloader.ts b/src/main/services/download/generic-http-downloader.ts
new file mode 100644
index 00000000..8384a5fd
--- /dev/null
+++ b/src/main/services/download/generic-http-downloader.ts
@@ -0,0 +1,107 @@
+import { Game } from "@main/entity";
+import { gameRepository } from "@main/repository";
+import { calculateETA } from "./helpers";
+import { DownloadProgress } from "@types";
+import { HttpDownload } from "./http-download";
+
+export class GenericHTTPDownloader {
+ private static downloads = new Map();
+ private static downloadingGame: Game | null = null;
+
+ public static async getStatus() {
+ if (this.downloadingGame) {
+ const gid = this.downloads.get(this.downloadingGame.id)!;
+ const status = HttpDownload.getStatus(gid);
+
+ if (status) {
+ const progress =
+ Number(status.completedLength) / Number(status.totalLength);
+
+ await gameRepository.update(
+ { id: this.downloadingGame!.id },
+ {
+ bytesDownloaded: Number(status.completedLength),
+ fileSize: Number(status.totalLength),
+ progress,
+ status: "active",
+ folderName: status.folderName,
+ }
+ );
+
+ const result = {
+ numPeers: 0,
+ numSeeds: 0,
+ downloadSpeed: status.downloadSpeed,
+ timeRemaining: calculateETA(
+ status.totalLength,
+ status.completedLength,
+ status.downloadSpeed
+ ),
+ isDownloadingMetadata: false,
+ isCheckingFiles: false,
+ progress,
+ gameId: this.downloadingGame!.id,
+ } as DownloadProgress;
+
+ if (progress === 1) {
+ this.downloads.delete(this.downloadingGame.id);
+ this.downloadingGame = null;
+ }
+
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ static async pauseDownload() {
+ if (this.downloadingGame) {
+ const gid = this.downloads.get(this.downloadingGame!.id!);
+
+ if (gid) {
+ await HttpDownload.pauseDownload(gid);
+ }
+
+ this.downloadingGame = null;
+ }
+ }
+
+ static async startDownload(
+ game: Game,
+ downloadUrl: string,
+ headers?: Record
+ ) {
+ this.downloadingGame = game;
+
+ if (this.downloads.has(game.id)) {
+ await this.resumeDownload(game.id!);
+ return;
+ }
+
+ const gid = await HttpDownload.startDownload(
+ game.downloadPath!,
+ downloadUrl,
+ headers
+ );
+
+ this.downloads.set(game.id!, gid);
+ }
+
+ static async cancelDownload(gameId: number) {
+ const gid = this.downloads.get(gameId);
+
+ if (gid) {
+ await HttpDownload.cancelDownload(gid);
+ this.downloads.delete(gameId);
+ }
+ }
+
+ static async resumeDownload(gameId: number) {
+ const gid = this.downloads.get(gameId);
+
+ if (gid) {
+ await HttpDownload.resumeDownload(gid);
+ }
+ }
+}
diff --git a/src/main/services/download/http-download.ts b/src/main/services/download/http-download.ts
index 4553a6cb..cd8cbee5 100644
--- a/src/main/services/download/http-download.ts
+++ b/src/main/services/download/http-download.ts
@@ -1,68 +1,69 @@
-import type { ChildProcess } from "node:child_process";
-import { logger } from "../logger";
-import { sleep } from "@main/helpers";
-import { startAria2 } from "../aria2c";
-import Aria2 from "aria2";
+import { DownloadItem } from "electron";
+import { WindowManager } from "../window-manager";
+import path from "node:path";
export class HttpDownload {
- private static connected = false;
- private static aria2c: ChildProcess | null = null;
+ private static id = 0;
- private static aria2 = new Aria2({});
+ private static downloads: Record = {};
- 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++;
- }
- }
- }
-
- public static getStatus(gid: string) {
- if (this.connected) {
- return this.aria2.call("tellStatus", gid);
+ public static getStatus(gid: string): {
+ completedLength: number;
+ totalLength: number;
+ downloadSpeed: number;
+ folderName: string;
+ } | null {
+ const downloadItem = this.downloads[gid];
+ if (downloadItem) {
+ return {
+ completedLength: downloadItem.getReceivedBytes(),
+ totalLength: downloadItem.getTotalBytes(),
+ downloadSpeed: downloadItem.getCurrentBytesPerSecond(),
+ folderName: downloadItem.getFilename(),
+ };
}
return null;
}
- public static disconnect() {
- if (this.aria2c) {
- this.aria2c.kill();
- this.connected = false;
- }
- }
-
static async cancelDownload(gid: string) {
- await this.aria2.call("forceRemove", gid);
+ const downloadItem = this.downloads[gid];
+ downloadItem?.cancel();
+ delete this.downloads[gid];
}
static async pauseDownload(gid: string) {
- await this.aria2.call("forcePause", gid);
+ const downloadItem = this.downloads[gid];
+ downloadItem?.pause();
}
static async resumeDownload(gid: string) {
- await this.aria2.call("unpause", gid);
+ const downloadItem = this.downloads[gid];
+ downloadItem?.resume();
}
- static async startDownload(downloadPath: string, downloadUrl: string) {
- if (!this.connected) await this.connect();
+ static async startDownload(
+ downloadPath: string,
+ downloadUrl: string,
+ headers?: Record
+ ) {
+ return new Promise((resolve) => {
+ const options = headers ? { headers } : {};
+ WindowManager.mainWindow?.webContents.downloadURL(downloadUrl, options);
- const options = {
- dir: downloadPath,
- };
+ const gid = ++this.id;
- return this.aria2.call("addUri", [downloadUrl], options);
+ WindowManager.mainWindow?.webContents.session.on(
+ "will-download",
+ (_event, item, _webContents) => {
+ this.downloads[gid.toString()] = item;
+
+ // Set the save path, making Electron not to prompt a save dialog.
+ item.setSavePath(path.join(downloadPath, item.getFilename()));
+
+ resolve(gid.toString());
+ }
+ );
+ });
}
}
diff --git a/src/main/services/download/real-debrid-downloader.ts b/src/main/services/download/real-debrid-downloader.ts
index 8ead0067..c6925f57 100644
--- a/src/main/services/download/real-debrid-downloader.ts
+++ b/src/main/services/download/real-debrid-downloader.ts
@@ -13,22 +13,36 @@ export class RealDebridDownloader {
private static async getRealDebridDownloadUrl() {
if (this.realDebridTorrentId) {
- const torrentInfo = await RealDebridClient.getTorrentInfo(
+ let torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId
);
- const { status, links } = torrentInfo;
-
- if (status === "waiting_files_selection") {
+ if (torrentInfo.status === "waiting_files_selection") {
await RealDebridClient.selectAllFiles(this.realDebridTorrentId);
- return null;
+
+ torrentInfo = await RealDebridClient.getTorrentInfo(
+ this.realDebridTorrentId
+ );
}
+ const { links, status } = torrentInfo;
+
if (status === "downloaded") {
const [link] = links;
+
const { download } = await RealDebridClient.unrestrictLink(link);
return decodeURIComponent(download);
}
+
+ return null;
+ }
+
+ if (this.downloadingGame?.uri) {
+ const { download } = await RealDebridClient.unrestrictLink(
+ this.downloadingGame?.uri
+ );
+
+ return decodeURIComponent(download);
}
return null;
@@ -37,7 +51,7 @@ export class RealDebridDownloader {
public static async getStatus() {
if (this.downloadingGame) {
const gid = this.downloads.get(this.downloadingGame.id)!;
- const status = await HttpDownload.getStatus(gid);
+ const status = HttpDownload.getStatus(gid);
if (status) {
const progress =
@@ -50,6 +64,7 @@ export class RealDebridDownloader {
fileSize: Number(status.totalLength),
progress,
status: "active",
+ folderName: status.folderName,
}
);
@@ -78,40 +93,15 @@ export class RealDebridDownloader {
}
}
- if (this.realDebridTorrentId && this.downloadingGame) {
- const torrentInfo = await RealDebridClient.getTorrentInfo(
- this.realDebridTorrentId
- );
-
- const { status } = torrentInfo;
-
- if (status === "downloaded") {
- this.startDownload(this.downloadingGame);
- }
-
- const progress = torrentInfo.progress / 100;
- const totalDownloaded = progress * torrentInfo.bytes;
-
- return {
- numPeers: 0,
- numSeeds: torrentInfo.seeders,
- downloadSpeed: torrentInfo.speed,
- timeRemaining: calculateETA(
- torrentInfo.bytes,
- totalDownloaded,
- torrentInfo.speed
- ),
- isDownloadingMetadata: status === "magnet_conversion",
- } as DownloadProgress;
- }
-
return null;
}
static async pauseDownload() {
- const gid = this.downloads.get(this.downloadingGame!.id!);
- if (gid) {
- await HttpDownload.pauseDownload(gid);
+ if (this.downloadingGame) {
+ const gid = this.downloads.get(this.downloadingGame.id);
+ if (gid) {
+ await HttpDownload.pauseDownload(gid);
+ }
}
this.realDebridTorrentId = null;
@@ -119,15 +109,19 @@ export class RealDebridDownloader {
}
static async startDownload(game: Game) {
- this.downloadingGame = game;
-
if (this.downloads.has(game.id)) {
await this.resumeDownload(game.id!);
-
+ this.downloadingGame = game;
return;
}
- this.realDebridTorrentId = await RealDebridClient.getTorrentId(game!.uri!);
+ if (game.uri?.startsWith("magnet:")) {
+ this.realDebridTorrentId = await RealDebridClient.getTorrentId(
+ game!.uri!
+ );
+ }
+
+ this.downloadingGame = game;
const downloadUrl = await this.getRealDebridDownloadUrl();
@@ -150,6 +144,9 @@ export class RealDebridDownloader {
await HttpDownload.cancelDownload(gid);
this.downloads.delete(gameId);
}
+
+ this.realDebridTorrentId = null;
+ this.downloadingGame = null;
}
static async resumeDownload(gameId: number) {
diff --git a/src/main/services/hosters/gofile.ts b/src/main/services/hosters/gofile.ts
new file mode 100644
index 00000000..770bb15f
--- /dev/null
+++ b/src/main/services/hosters/gofile.ts
@@ -0,0 +1,61 @@
+import axios from "axios";
+
+export interface GofileAccountsReponse {
+ id: string;
+ token: string;
+}
+
+export interface GofileContentChild {
+ id: string;
+ link: string;
+}
+
+export interface GofileContentsResponse {
+ id: string;
+ type: string;
+ children: Record;
+}
+
+export class GofileApi {
+ private static token: string;
+
+ public static async authorize() {
+ const response = await axios.post<{
+ status: string;
+ data: GofileAccountsReponse;
+ }>("https://api.gofile.io/accounts");
+
+ if (response.data.status === "ok") {
+ this.token = response.data.data.token;
+ return this.token;
+ }
+
+ throw new Error("Failed to authorize");
+ }
+
+ public static async getDownloadLink(id: string) {
+ const searchParams = new URLSearchParams({
+ wt: "4fd6sg89d7s6",
+ });
+
+ const response = await axios.get<{
+ status: string;
+ data: GofileContentsResponse;
+ }>(`https://api.gofile.io/contents/${id}?${searchParams.toString()}`, {
+ headers: {
+ Authorization: `Bearer ${this.token}`,
+ },
+ });
+
+ if (response.data.status === "ok") {
+ if (response.data.data.type !== "folder") {
+ throw new Error("Only folders are supported");
+ }
+
+ const [firstChild] = Object.values(response.data.data.children);
+ return firstChild.link;
+ }
+
+ throw new Error("Failed to get download link");
+ }
+}
diff --git a/src/main/services/hosters/index.ts b/src/main/services/hosters/index.ts
new file mode 100644
index 00000000..921c45b1
--- /dev/null
+++ b/src/main/services/hosters/index.ts
@@ -0,0 +1 @@
+export * from "./gofile";
diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts
index 5365bd9e..120d27ac 100644
--- a/src/main/services/hydra-api.ts
+++ b/src/main/services/hydra-api.ts
@@ -64,6 +64,14 @@ export class HydraApi {
}
}
+ static handleSignOut() {
+ this.userAuth = {
+ authToken: "",
+ refreshToken: "",
+ expirationTimestamp: 0,
+ };
+ }
+
static async setupApi() {
this.instance = axios.create({
baseURL: import.meta.env.MAIN_VITE_API_URL,
diff --git a/src/main/services/real-debrid.ts b/src/main/services/real-debrid.ts
index 2e0debe6..26ba4c79 100644
--- a/src/main/services/real-debrid.ts
+++ b/src/main/services/real-debrid.ts
@@ -46,7 +46,7 @@ export class RealDebridClient {
static async selectAllFiles(id: string) {
const searchParams = new URLSearchParams({ files: "all" });
- await this.instance.post(
+ return this.instance.post(
`/torrents/selectFiles/${id}`,
searchParams.toString()
);
diff --git a/src/renderer/src/app.css.ts b/src/renderer/src/app.css.ts
index a5f9394b..c829021a 100644
--- a/src/renderer/src/app.css.ts
+++ b/src/renderer/src/app.css.ts
@@ -26,7 +26,7 @@ globalStyle("html, body, #root, main", {
globalStyle("body", {
overflow: "hidden",
userSelect: "none",
- fontFamily: "'Fira Mono', monospace",
+ fontFamily: "Noto Sans, sans-serif",
fontSize: vars.size.body,
background: vars.color.background,
color: vars.color.body,
diff --git a/src/renderer/src/components/hero/hero.css.ts b/src/renderer/src/components/hero/hero.css.ts
index cdb36ee2..eaf0a101 100644
--- a/src/renderer/src/components/hero/hero.css.ts
+++ b/src/renderer/src/components/hero/hero.css.ts
@@ -45,7 +45,6 @@ export const description = style({
maxWidth: "700px",
color: vars.color.muted,
textAlign: "left",
- fontFamily: "'Fira Sans', sans-serif",
lineHeight: "20px",
marginTop: `${SPACING_UNIT * 2}px`,
});
diff --git a/src/renderer/src/components/modal/modal.css.ts b/src/renderer/src/components/modal/modal.css.ts
index 45154015..d9d14fda 100644
--- a/src/renderer/src/components/modal/modal.css.ts
+++ b/src/renderer/src/components/modal/modal.css.ts
@@ -24,6 +24,7 @@ export const modal = recipe({
animation: `${scaleFadeIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`,
backgroundColor: vars.color.background,
borderRadius: "4px",
+ minWidth: "400px",
maxWidth: "600px",
color: vars.color.body,
maxHeight: "100%",
diff --git a/src/renderer/src/constants.ts b/src/renderer/src/constants.ts
index 6186bb85..7025df2a 100644
--- a/src/renderer/src/constants.ts
+++ b/src/renderer/src/constants.ts
@@ -5,4 +5,6 @@ export const VERSION_CODENAME = "Leviticus";
export const DOWNLOADER_NAME = {
[Downloader.RealDebrid]: "Real-Debrid",
[Downloader.Torrent]: "Torrent",
+ [Downloader.Gofile]: "Gofile",
+ [Downloader.PixelDrain]: "PixelDrain",
};
diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx
index f87d66bf..b88348f0 100644
--- a/src/renderer/src/main.tsx
+++ b/src/renderer/src/main.tsx
@@ -8,12 +8,10 @@ import { HashRouter, Route, Routes } from "react-router-dom";
import * as Sentry from "@sentry/electron/renderer";
-import "@fontsource/fira-mono/400.css";
-import "@fontsource/fira-mono/500.css";
-import "@fontsource/fira-mono/700.css";
-import "@fontsource/fira-sans/400.css";
-import "@fontsource/fira-sans/500.css";
-import "@fontsource/fira-sans/700.css";
+import "@fontsource/noto-sans/400.css";
+import "@fontsource/noto-sans/500.css";
+import "@fontsource/noto-sans/700.css";
+
import "react-loading-skeleton/dist/skeleton.css";
import { App } from "./app";
diff --git a/src/renderer/src/pages/downloads/downloads.tsx b/src/renderer/src/pages/downloads/downloads.tsx
index 531bc526..5a9c228c 100644
--- a/src/renderer/src/pages/downloads/downloads.tsx
+++ b/src/renderer/src/pages/downloads/downloads.tsx
@@ -132,9 +132,7 @@ export function Downloads() {
{t("no_downloads_title")}
-
- {t("no_downloads_description")}
-
+ {t("no_downloads_description")}
)}
>
diff --git a/src/renderer/src/pages/game-details/description-header/description-header.tsx b/src/renderer/src/pages/game-details/description-header/description-header.tsx
index e4272534..cd73c52a 100644
--- a/src/renderer/src/pages/game-details/description-header/description-header.tsx
+++ b/src/renderer/src/pages/game-details/description-header/description-header.tsx
@@ -19,7 +19,10 @@ export function DescriptionHeader() {
date: shopDetails?.release_date.date,
})}
- {t("publisher", { publisher: shopDetails.publishers[0] })}
+
+ {Array.isArray(shopDetails.publishers) && (
+ {t("publisher", { publisher: shopDetails.publishers[0] })}
+ )}
);
diff --git a/src/renderer/src/pages/game-details/game-details.css.ts b/src/renderer/src/pages/game-details/game-details.css.ts
index f0bbfd2e..3dc0ec94 100644
--- a/src/renderer/src/pages/game-details/game-details.css.ts
+++ b/src/renderer/src/pages/game-details/game-details.css.ts
@@ -101,7 +101,6 @@ export const descriptionContent = style({
export const description = style({
userSelect: "text",
lineHeight: "22px",
- fontFamily: "'Fira Sans', sans-serif",
fontSize: "16px",
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
"@media": {
diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.css.ts b/src/renderer/src/pages/game-details/hero/hero-panel.css.ts
index e10d55a5..c379c1c3 100644
--- a/src/renderer/src/pages/game-details/hero/hero-panel.css.ts
+++ b/src/renderer/src/pages/game-details/hero/hero-panel.css.ts
@@ -9,6 +9,7 @@ export const panel = recipe({
height: "72px",
minHeight: "72px",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
+ backgroundColor: vars.color.darkBackground,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx
index ef4ba040..dff73ea0 100644
--- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx
+++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx
@@ -1,11 +1,11 @@
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { DiskSpace } from "check-disk-space";
import * as styles from "./download-settings-modal.css";
import { Button, Link, Modal, TextField } from "@renderer/components";
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
-import { Downloader, formatBytes } from "@shared";
+import { Downloader, formatBytes, getDownloadersForUri } from "@shared";
import type { GameRepack } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css";
@@ -23,8 +23,6 @@ export interface DownloadSettingsModalProps {
repack: GameRepack | null;
}
-const downloaders = [Downloader.Torrent, Downloader.RealDebrid];
-
export function DownloadSettingsModal({
visible,
onClose,
@@ -36,9 +34,8 @@ export function DownloadSettingsModal({
const [diskFreeSpace, setDiskFreeSpace] = useState(null);
const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false);
- const [selectedDownloader, setSelectedDownloader] = useState(
- Downloader.Torrent
- );
+ const [selectedDownloader, setSelectedDownloader] =
+ useState(null);
const userPreferences = useAppSelector(
(state) => state.userPreferences.value
@@ -50,6 +47,10 @@ export function DownloadSettingsModal({
}
}, [visible, selectedPath]);
+ const downloaders = useMemo(() => {
+ return getDownloadersForUri(repack?.magnet ?? "");
+ }, [repack?.magnet]);
+
useEffect(() => {
if (userPreferences?.downloadsPath) {
setSelectedPath(userPreferences.downloadsPath);
@@ -59,9 +60,27 @@ export function DownloadSettingsModal({
.then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath));
}
- if (userPreferences?.realDebridApiToken)
- setSelectedDownloader(Downloader.RealDebrid);
- }, [userPreferences?.downloadsPath, userPreferences?.realDebridApiToken]);
+ const filteredDownloaders = downloaders.filter((downloader) => {
+ if (downloader === Downloader.RealDebrid)
+ return userPreferences?.realDebridApiToken;
+ return true;
+ });
+
+ /* Gives preference to Real Debrid */
+ const selectedDownloader = filteredDownloaders.includes(
+ Downloader.RealDebrid
+ )
+ ? Downloader.RealDebrid
+ : filteredDownloaders[0];
+
+ setSelectedDownloader(
+ selectedDownloader === undefined ? null : selectedDownloader
+ );
+ }, [
+ userPreferences?.downloadsPath,
+ downloaders,
+ userPreferences?.realDebridApiToken,
+ ]);
const getDiskFreeSpace = (path: string) => {
window.electron.getDiskFreeSpace(path).then((result) => {
@@ -85,7 +104,7 @@ export function DownloadSettingsModal({
if (repack) {
setDownloadStarting(true);
- startDownload(repack, selectedDownloader, selectedPath).finally(() => {
+ startDownload(repack, selectedDownloader!, selectedPath).finally(() => {
setDownloadStarting(false);
onClose();
});
@@ -167,7 +186,10 @@ export function DownloadSettingsModal({
-