feat: adding generic http downloads

This commit is contained in:
Chubby Granny Chaser 2024-07-09 19:24:02 +01:00
parent 6fce60f9f7
commit 6c6fff71fe
No known key found for this signature in database
12 changed files with 294 additions and 41 deletions

View file

@ -44,6 +44,8 @@ const startGameDownload = async (
); );
if (game) { if (game) {
console.log("game", game);
await gameRepository.update( await gameRepository.update(
{ {
id: game.id, id: game.id,
@ -95,6 +97,8 @@ const startGameDownload = async (
}, },
}); });
console.log(updatedGame);
createGame(updatedGame!); createGame(updatedGame!);
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } }); await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });

View file

@ -22,8 +22,9 @@ const loadState = async (userPreferences: UserPreferences | null) => {
import("./events"); import("./events");
if (userPreferences?.realDebridApiToken) if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences?.realDebridApiToken); RealDebridClient.authorize(userPreferences?.realDebridApiToken);
}
HydraApi.setupApi().then(() => { HydraApi.setupApi().then(() => {
uploadGamesBatch(); uploadGamesBatch();

View file

@ -6,6 +6,8 @@ import { downloadQueueRepository, gameRepository } from "@main/repository";
import { publishDownloadCompleteNotification } from "../notifications"; import { publishDownloadCompleteNotification } from "../notifications";
import { RealDebridDownloader } from "./real-debrid-downloader"; import { RealDebridDownloader } from "./real-debrid-downloader";
import type { DownloadProgress } from "@types"; import type { DownloadProgress } from "@types";
import { GofileApi } from "../hosters";
import { GenericHTTPDownloader } from "./generic-http-downloader";
export class DownloadManager { export class DownloadManager {
private static currentDownloader: Downloader | null = null; private static currentDownloader: Downloader | null = null;
@ -13,10 +15,12 @@ export class DownloadManager {
public static async watchDownloads() { public static async watchDownloads() {
let status: DownloadProgress | null = null; 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(); status = await RealDebridDownloader.getStatus();
} else { } else {
status = await PythonInstance.getStatus(); status = await GenericHTTPDownloader.getStatus();
} }
if (status) { if (status) {
@ -62,10 +66,12 @@ export class DownloadManager {
} }
static async pauseDownload() { 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(); await RealDebridDownloader.pauseDownload();
} else { } else {
await PythonInstance.pauseDownload(); await GenericHTTPDownloader.pauseDownload();
} }
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
@ -73,20 +79,16 @@ export class DownloadManager {
} }
static async resumeDownload(game: Game) { static async resumeDownload(game: Game) {
if (game.downloader === Downloader.RealDebrid) { return this.startDownload(game);
RealDebridDownloader.startDownload(game);
this.currentDownloader = Downloader.RealDebrid;
} else {
PythonInstance.startDownload(game);
this.currentDownloader = Downloader.Torrent;
}
} }
static async cancelDownload(gameId: number) { 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); RealDebridDownloader.cancelDownload(gameId);
} else { } else {
PythonInstance.cancelDownload(gameId); GenericHTTPDownloader.cancelDownload(gameId);
} }
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
@ -94,12 +96,30 @@ export class DownloadManager {
} }
static async startDownload(game: Game) { static async startDownload(game: Game) {
if (game.downloader === Downloader.RealDebrid) { if (game.downloader === Downloader.Gofile) {
RealDebridDownloader.startDownload(game); const id = game!.uri!.split("/").pop();
this.currentDownloader = Downloader.RealDebrid;
} else { 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); PythonInstance.startDownload(game);
this.currentDownloader = Downloader.Torrent; } else if (game.downloader === Downloader.RealDebrid) {
} RealDebridDownloader.startDownload(game);
}
this.currentDownloader = game.downloader;
} }
} }

View file

@ -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<number, string>();
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);
}
}
}

View file

@ -4,7 +4,7 @@ import { sleep } from "@main/helpers";
import { startAria2 } from "../aria2c"; import { startAria2 } from "../aria2c";
import Aria2 from "aria2"; import Aria2 from "aria2";
export class HttpDownload { export class HTTPDownload {
private static connected = false; private static connected = false;
private static aria2c: ChildProcess | null = null; private static aria2c: ChildProcess | null = null;
@ -56,11 +56,17 @@ export class HttpDownload {
await this.aria2.call("unpause", gid); 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(); if (!this.connected) await this.connect();
const options = { const options = {
dir: downloadPath, dir: downloadPath,
header,
}; };
return this.aria2.call("addUri", [downloadUrl], options); return this.aria2.call("addUri", [downloadUrl], options);

View file

@ -3,7 +3,7 @@ import { RealDebridClient } from "../real-debrid";
import { gameRepository } from "@main/repository"; import { gameRepository } from "@main/repository";
import { calculateETA } from "./helpers"; import { calculateETA } from "./helpers";
import { DownloadProgress } from "@types"; import { DownloadProgress } from "@types";
import { HttpDownload } from "./http-download"; import { HTTPDownload } from "./http-download";
export class RealDebridDownloader { export class RealDebridDownloader {
private static downloads = new Map<number, string>(); private static downloads = new Map<number, string>();
@ -29,6 +29,18 @@ export class RealDebridDownloader {
const { download } = await RealDebridClient.unrestrictLink(link); const { download } = await RealDebridClient.unrestrictLink(link);
return decodeURIComponent(download); 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; return null;
@ -37,7 +49,7 @@ export class RealDebridDownloader {
public static async getStatus() { public static async getStatus() {
if (this.downloadingGame) { if (this.downloadingGame) {
const gid = this.downloads.get(this.downloadingGame.id)!; const gid = this.downloads.get(this.downloadingGame.id)!;
const status = await HttpDownload.getStatus(gid); const status = await HTTPDownload.getStatus(gid);
if (status) { if (status) {
const progress = const progress =
@ -111,7 +123,7 @@ export class RealDebridDownloader {
static async pauseDownload() { static async pauseDownload() {
const gid = this.downloads.get(this.downloadingGame!.id!); const gid = this.downloads.get(this.downloadingGame!.id!);
if (gid) { if (gid) {
await HttpDownload.pauseDownload(gid); await HTTPDownload.pauseDownload(gid);
} }
this.realDebridTorrentId = null; this.realDebridTorrentId = null;
@ -127,14 +139,18 @@ export class RealDebridDownloader {
return; 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(); const downloadUrl = await this.getRealDebridDownloadUrl();
if (downloadUrl) { if (downloadUrl) {
this.realDebridTorrentId = null; this.realDebridTorrentId = null;
const gid = await HttpDownload.startDownload( const gid = await HTTPDownload.startDownload(
game.downloadPath!, game.downloadPath!,
downloadUrl downloadUrl
); );
@ -147,7 +163,7 @@ export class RealDebridDownloader {
const gid = this.downloads.get(gameId); const gid = this.downloads.get(gameId);
if (gid) { if (gid) {
await HttpDownload.cancelDownload(gid); await HTTPDownload.cancelDownload(gid);
this.downloads.delete(gameId); this.downloads.delete(gameId);
} }
} }
@ -156,7 +172,7 @@ export class RealDebridDownloader {
const gid = this.downloads.get(gameId); const gid = this.downloads.get(gameId);
if (gid) { if (gid) {
await HttpDownload.resumeDownload(gid); await HTTPDownload.resumeDownload(gid);
} }
} }
} }

View file

@ -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<string, GofileContentChild>;
}
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");
}
}

View file

@ -0,0 +1 @@
export * from "./gofile";

View file

@ -5,4 +5,6 @@ export const VERSION_CODENAME = "Leviticus";
export const DOWNLOADER_NAME = { export const DOWNLOADER_NAME = {
[Downloader.RealDebrid]: "Real-Debrid", [Downloader.RealDebrid]: "Real-Debrid",
[Downloader.Torrent]: "Torrent", [Downloader.Torrent]: "Torrent",
[Downloader.Gofile]: "Gofile",
[Downloader.PixelDrain]: "PixelDrain",
}; };

View file

@ -1,11 +1,11 @@
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { DiskSpace } from "check-disk-space"; import { DiskSpace } from "check-disk-space";
import * as styles from "./download-settings-modal.css"; import * as styles from "./download-settings-modal.css";
import { Button, Link, Modal, TextField } from "@renderer/components"; import { Button, Link, Modal, TextField } from "@renderer/components";
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react"; import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
import { Downloader, formatBytes } from "@shared"; import { Downloader, formatBytes, getDownloadersForUri } from "@shared";
import type { GameRepack } from "@types"; import type { GameRepack } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
@ -23,8 +23,6 @@ export interface DownloadSettingsModalProps {
repack: GameRepack | null; repack: GameRepack | null;
} }
const downloaders = [Downloader.Torrent, Downloader.RealDebrid];
export function DownloadSettingsModal({ export function DownloadSettingsModal({
visible, visible,
onClose, onClose,
@ -36,9 +34,8 @@ export function DownloadSettingsModal({
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null); const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
const [selectedPath, setSelectedPath] = useState(""); const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false); const [downloadStarting, setDownloadStarting] = useState(false);
const [selectedDownloader, setSelectedDownloader] = useState( const [selectedDownloader, setSelectedDownloader] =
Downloader.Torrent useState<Downloader | null>(null);
);
const userPreferences = useAppSelector( const userPreferences = useAppSelector(
(state) => state.userPreferences.value (state) => state.userPreferences.value
@ -50,6 +47,10 @@ export function DownloadSettingsModal({
} }
}, [visible, selectedPath]); }, [visible, selectedPath]);
const downloaders = useMemo(() => {
return getDownloadersForUri(repack?.magnet ?? "");
}, [repack?.magnet]);
useEffect(() => { useEffect(() => {
if (userPreferences?.downloadsPath) { if (userPreferences?.downloadsPath) {
setSelectedPath(userPreferences.downloadsPath); setSelectedPath(userPreferences.downloadsPath);
@ -59,9 +60,19 @@ export function DownloadSettingsModal({
.then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath)); .then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath));
} }
if (userPreferences?.realDebridApiToken) if (
userPreferences?.realDebridApiToken &&
downloaders.includes(Downloader.RealDebrid)
) {
setSelectedDownloader(Downloader.RealDebrid); setSelectedDownloader(Downloader.RealDebrid);
}, [userPreferences?.downloadsPath, userPreferences?.realDebridApiToken]); } else {
setSelectedDownloader(downloaders[0]);
}
}, [
userPreferences?.downloadsPath,
downloaders,
userPreferences?.realDebridApiToken,
]);
const getDiskFreeSpace = (path: string) => { const getDiskFreeSpace = (path: string) => {
window.electron.getDiskFreeSpace(path).then((result) => { window.electron.getDiskFreeSpace(path).then((result) => {
@ -85,7 +96,7 @@ export function DownloadSettingsModal({
if (repack) { if (repack) {
setDownloadStarting(true); setDownloadStarting(true);
startDownload(repack, selectedDownloader, selectedPath).finally(() => { startDownload(repack, selectedDownloader!, selectedPath).finally(() => {
setDownloadStarting(false); setDownloadStarting(false);
onClose(); onClose();
}); });
@ -167,7 +178,10 @@ export function DownloadSettingsModal({
</p> </p>
</div> </div>
<Button onClick={handleStartClick} disabled={downloadStarting}> <Button
onClick={handleStartClick}
disabled={downloadStarting || selectedDownloader === null}
>
<DownloadIcon /> <DownloadIcon />
{t("download_now")} {t("download_now")}
</Button> </Button>

View file

@ -44,8 +44,10 @@ export function RepacksModal({
}, [repacks]); }, [repacks]);
const getInfoHash = useCallback(async () => { const getInfoHash = useCallback(async () => {
if (game?.uri?.startsWith("magnet:")) {
const torrent = await parseTorrent(game?.uri ?? ""); const torrent = await parseTorrent(game?.uri ?? "");
if (torrent.infoHash) setInfoHash(torrent.infoHash); if (torrent.infoHash) setInfoHash(torrent.infoHash);
}
}, [game]); }, [game]);
useEffect(() => { useEffect(() => {

View file

@ -1,6 +1,8 @@
export enum Downloader { export enum Downloader {
RealDebrid, RealDebrid,
Torrent, Torrent,
Gofile,
PixelDrain,
} }
export enum DownloadSourceStatus { export enum DownloadSourceStatus {
@ -63,3 +65,18 @@ export const formatName = pipe<string>(
removeDuplicateSpaces, removeDuplicateSpaces,
(str) => str.trim() (str) => str.trim()
); );
const realDebridHosts = ["https://1fichier.com", "https://mediafire.com"];
export const getDownloadersForUri = (uri: string) => {
if (uri.startsWith("https://gofile.io")) return [Downloader.Gofile];
if (uri.startsWith("https://pixeldrain.com")) return [Downloader.PixelDrain];
if (realDebridHosts.some((host) => uri.startsWith(host)))
return [Downloader.RealDebrid];
if (uri.startsWith("magnet:"))
return [Downloader.Torrent, Downloader.RealDebrid];
return [];
};