From 6c6fff71fe145cf68e0817d7c2b8ec2364a611e4 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Tue, 9 Jul 2024 19:24:02 +0100
Subject: [PATCH 01/33] feat: adding generic http downloads
---
.../events/torrenting/start-game-download.ts | 4 +
src/main/main.ts | 3 +-
.../services/download/download-manager.ts | 56 ++++++---
.../download/generic-http-downloader.ts | 109 ++++++++++++++++++
src/main/services/download/http-download.ts | 10 +-
.../download/real-debrid-downloader.ts | 30 +++--
src/main/services/hosters/gofile.ts | 61 ++++++++++
src/main/services/hosters/index.ts | 1 +
src/renderer/src/constants.ts | 2 +
.../modals/download-settings-modal.tsx | 36 ++++--
.../game-details/modals/repacks-modal.tsx | 6 +-
src/shared/index.ts | 17 +++
12 files changed, 294 insertions(+), 41 deletions(-)
create mode 100644 src/main/services/download/generic-http-downloader.ts
create mode 100644 src/main/services/hosters/gofile.ts
create mode 100644 src/main/services/hosters/index.ts
diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts
index cea41596..aa33c99a 100644
--- a/src/main/events/torrenting/start-game-download.ts
+++ b/src/main/events/torrenting/start-game-download.ts
@@ -44,6 +44,8 @@ const startGameDownload = async (
);
if (game) {
+ console.log("game", game);
+
await gameRepository.update(
{
id: game.id,
@@ -95,6 +97,8 @@ const startGameDownload = async (
},
});
+ console.log(updatedGame);
+
createGame(updatedGame!);
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
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/download/download-manager.ts b/src/main/services/download/download-manager.ts
index 31f28992..d6542396 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,30 @@ 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!);
+
+ console.log(downloadLink, token, "<<<");
+
+ 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..688769a4
--- /dev/null
+++ b/src/main/services/download/generic-http-downloader.ts
@@ -0,0 +1,109 @@
+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 = await 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",
+ }
+ );
+
+ const result = {
+ numPeers: 0,
+ numSeeds: 0,
+ downloadSpeed: Number(status.downloadSpeed),
+ timeRemaining: calculateETA(
+ Number(status.totalLength),
+ Number(status.completedLength),
+ Number(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: string[] = []
+ ) {
+ this.downloadingGame = game;
+
+ if (this.downloads.has(game.id)) {
+ await this.resumeDownload(game.id!);
+
+ return;
+ }
+
+ if (downloadUrl) {
+ 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..d147e208 100644
--- a/src/main/services/download/http-download.ts
+++ b/src/main/services/download/http-download.ts
@@ -4,7 +4,7 @@ import { sleep } from "@main/helpers";
import { startAria2 } from "../aria2c";
import Aria2 from "aria2";
-export class HttpDownload {
+export class HTTPDownload {
private static connected = false;
private static aria2c: ChildProcess | null = null;
@@ -56,11 +56,17 @@ export class HttpDownload {
await this.aria2.call("unpause", gid);
}
- static async startDownload(downloadPath: string, downloadUrl: string) {
+ static async startDownload(
+ downloadPath: string,
+ downloadUrl: string,
+ header: string[] = []
+ ) {
+ console.log(header);
if (!this.connected) await this.connect();
const options = {
dir: downloadPath,
+ header,
};
return this.aria2.call("addUri", [downloadUrl], options);
diff --git a/src/main/services/download/real-debrid-downloader.ts b/src/main/services/download/real-debrid-downloader.ts
index 8ead0067..034ffc49 100644
--- a/src/main/services/download/real-debrid-downloader.ts
+++ b/src/main/services/download/real-debrid-downloader.ts
@@ -3,7 +3,7 @@ import { RealDebridClient } from "../real-debrid";
import { gameRepository } from "@main/repository";
import { calculateETA } from "./helpers";
import { DownloadProgress } from "@types";
-import { HttpDownload } from "./http-download";
+import { HTTPDownload } from "./http-download";
export class RealDebridDownloader {
private static downloads = new Map();
@@ -29,6 +29,18 @@ export class RealDebridDownloader {
const { download } = await RealDebridClient.unrestrictLink(link);
return decodeURIComponent(download);
}
+
+ return null;
+ }
+
+ if (this.downloadingGame?.uri) {
+ const { download } = await RealDebridClient.unrestrictLink(
+ this.downloadingGame?.uri
+ );
+
+ console.log("download>>", download);
+
+ return decodeURIComponent(download);
}
return null;
@@ -37,7 +49,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 = await HTTPDownload.getStatus(gid);
if (status) {
const progress =
@@ -111,7 +123,7 @@ export class RealDebridDownloader {
static async pauseDownload() {
const gid = this.downloads.get(this.downloadingGame!.id!);
if (gid) {
- await HttpDownload.pauseDownload(gid);
+ await HTTPDownload.pauseDownload(gid);
}
this.realDebridTorrentId = null;
@@ -127,14 +139,18 @@ export class RealDebridDownloader {
return;
}
- this.realDebridTorrentId = await RealDebridClient.getTorrentId(game!.uri!);
+ if (game.uri?.startsWith("magnet:")) {
+ this.realDebridTorrentId = await RealDebridClient.getTorrentId(
+ game!.uri!
+ );
+ }
const downloadUrl = await this.getRealDebridDownloadUrl();
if (downloadUrl) {
this.realDebridTorrentId = null;
- const gid = await HttpDownload.startDownload(
+ const gid = await HTTPDownload.startDownload(
game.downloadPath!,
downloadUrl
);
@@ -147,7 +163,7 @@ export class RealDebridDownloader {
const gid = this.downloads.get(gameId);
if (gid) {
- await HttpDownload.cancelDownload(gid);
+ await HTTPDownload.cancelDownload(gid);
this.downloads.delete(gameId);
}
}
@@ -156,7 +172,7 @@ export class RealDebridDownloader {
const gid = this.downloads.get(gameId);
if (gid) {
- await HttpDownload.resumeDownload(gid);
+ await HTTPDownload.resumeDownload(gid);
}
}
}
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/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/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx
index ef4ba040..d102d2b2 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,19 @@ export function DownloadSettingsModal({
.then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath));
}
- if (userPreferences?.realDebridApiToken)
+ if (
+ userPreferences?.realDebridApiToken &&
+ downloaders.includes(Downloader.RealDebrid)
+ ) {
setSelectedDownloader(Downloader.RealDebrid);
- }, [userPreferences?.downloadsPath, userPreferences?.realDebridApiToken]);
+ } else {
+ setSelectedDownloader(downloaders[0]);
+ }
+ }, [
+ userPreferences?.downloadsPath,
+ downloaders,
+ userPreferences?.realDebridApiToken,
+ ]);
const getDiskFreeSpace = (path: string) => {
window.electron.getDiskFreeSpace(path).then((result) => {
@@ -85,7 +96,7 @@ export function DownloadSettingsModal({
if (repack) {
setDownloadStarting(true);
- startDownload(repack, selectedDownloader, selectedPath).finally(() => {
+ startDownload(repack, selectedDownloader!, selectedPath).finally(() => {
setDownloadStarting(false);
onClose();
});
@@ -167,7 +178,10 @@ export function DownloadSettingsModal({
-