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/12] 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 cea41596f..aa33c99ab 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 fbabc56cd..af594e206 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 31f28992a..d6542396f 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 000000000..688769a4e --- /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 4553a6cbf..d147e208c 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 8ead0067a..034ffc492 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 000000000..770bb15f9 --- /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 000000000..921c45b11 --- /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 6186bb854..7025df2a0 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 ef4ba0403..d102d2b2d 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({
- {t("no_downloads_description")} -
{t("no_downloads_description")}
{t("publisher", { publisher: shopDetails.publishers[0] })}
- {t("download_sources_description")} -
{t("download_sources_description")}
- {t("user_block_modal_text", { displayName })} -
{t("user_block_modal_text", { displayName })}
- {t("no_recent_activity_description")} -
{t("no_recent_activity_description")}
{t("sign_out_modal_text")}