mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
357 lines
9.8 KiB
TypeScript
357 lines
9.8 KiB
TypeScript
import { Downloader, DownloadError } from "@shared";
|
|
import { WindowManager } from "../window-manager";
|
|
import { publishDownloadCompleteNotification } from "../notifications";
|
|
import type { Download, DownloadProgress, UserPreferences } from "@types";
|
|
import { GofileApi, QiwiApi, DatanodesApi, MediafireApi } from "../hosters";
|
|
import { PythonRPC } from "../python-rpc";
|
|
import {
|
|
LibtorrentPayload,
|
|
LibtorrentStatus,
|
|
PauseDownloadPayload,
|
|
} from "./types";
|
|
import { calculateETA, getDirSize } from "./helpers";
|
|
import { RealDebridClient } from "./real-debrid";
|
|
import path from "path";
|
|
import { logger } from "../logger";
|
|
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
|
import { sortBy } from "lodash-es";
|
|
import { TorBoxClient } from "./torbox";
|
|
|
|
export class DownloadManager {
|
|
private static downloadingGameId: string | null = null;
|
|
|
|
public static async startRPC(
|
|
download?: Download,
|
|
downloadsToSeed?: Download[]
|
|
) {
|
|
PythonRPC.spawn(
|
|
download?.status === "active"
|
|
? await this.getDownloadPayload(download).catch((err) => {
|
|
logger.error("Error getting download payload", err);
|
|
return undefined;
|
|
})
|
|
: undefined,
|
|
downloadsToSeed?.map((download) => ({
|
|
game_id: levelKeys.game(download.shop, download.objectId),
|
|
url: download.uri,
|
|
save_path: download.downloadPath,
|
|
}))
|
|
);
|
|
|
|
if (download) {
|
|
this.downloadingGameId = levelKeys.game(download.shop, download.objectId);
|
|
}
|
|
}
|
|
|
|
private static async getDownloadStatus() {
|
|
const response = await PythonRPC.rpc.get<LibtorrentPayload | null>(
|
|
"/status"
|
|
);
|
|
if (response.data === null || !this.downloadingGameId) return null;
|
|
const downloadId = this.downloadingGameId;
|
|
|
|
try {
|
|
const {
|
|
progress,
|
|
numPeers,
|
|
numSeeds,
|
|
downloadSpeed,
|
|
bytesDownloaded,
|
|
fileSize,
|
|
folderName,
|
|
status,
|
|
} = response.data;
|
|
|
|
const isDownloadingMetadata =
|
|
status === LibtorrentStatus.DownloadingMetadata;
|
|
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
|
|
|
|
const download = await downloadsSublevel.get(downloadId);
|
|
|
|
if (!isDownloadingMetadata && !isCheckingFiles) {
|
|
if (!download) return null;
|
|
|
|
await downloadsSublevel.put(downloadId, {
|
|
...download,
|
|
bytesDownloaded,
|
|
fileSize,
|
|
progress,
|
|
folderName,
|
|
status: "active",
|
|
});
|
|
}
|
|
|
|
return {
|
|
numPeers,
|
|
numSeeds,
|
|
downloadSpeed,
|
|
timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed),
|
|
isDownloadingMetadata,
|
|
isCheckingFiles,
|
|
progress,
|
|
gameId: downloadId,
|
|
download,
|
|
} as DownloadProgress;
|
|
} catch (err) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static async watchDownloads() {
|
|
const status = await this.getDownloadStatus();
|
|
|
|
if (status) {
|
|
const { gameId, progress } = status;
|
|
|
|
const [download, game] = await Promise.all([
|
|
downloadsSublevel.get(gameId),
|
|
gamesSublevel.get(gameId),
|
|
]);
|
|
|
|
if (!download || !game) return;
|
|
|
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
|
levelKeys.userPreferences,
|
|
{
|
|
valueEncoding: "json",
|
|
}
|
|
);
|
|
|
|
if (WindowManager.mainWindow && download) {
|
|
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
|
WindowManager.mainWindow.webContents.send(
|
|
"on-download-progress",
|
|
JSON.parse(
|
|
JSON.stringify({
|
|
...status,
|
|
game,
|
|
})
|
|
)
|
|
);
|
|
}
|
|
|
|
if (progress === 1 && download) {
|
|
publishDownloadCompleteNotification(game);
|
|
|
|
if (
|
|
userPreferences?.seedAfterDownloadComplete &&
|
|
download.downloader === Downloader.Torrent
|
|
) {
|
|
downloadsSublevel.put(gameId, {
|
|
...download,
|
|
status: "seeding",
|
|
shouldSeed: true,
|
|
queued: false,
|
|
});
|
|
} else {
|
|
downloadsSublevel.put(gameId, {
|
|
...download,
|
|
status: "complete",
|
|
shouldSeed: false,
|
|
queued: false,
|
|
});
|
|
|
|
this.cancelDownload(gameId);
|
|
}
|
|
|
|
const downloads = await downloadsSublevel
|
|
.values()
|
|
.all()
|
|
.then((games) => {
|
|
return sortBy(
|
|
games.filter((game) => game.status === "paused" && game.queued),
|
|
"timestamp",
|
|
"DESC"
|
|
);
|
|
});
|
|
|
|
const [nextItemOnQueue] = downloads;
|
|
|
|
if (nextItemOnQueue) {
|
|
this.resumeDownload(nextItemOnQueue);
|
|
} else {
|
|
this.downloadingGameId = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static async getSeedStatus() {
|
|
const seedStatus = await PythonRPC.rpc
|
|
.get<LibtorrentPayload[] | []>("/seed-status")
|
|
.then((res) => res.data);
|
|
|
|
if (!seedStatus.length) return;
|
|
|
|
logger.log(seedStatus);
|
|
|
|
seedStatus.forEach(async (status) => {
|
|
const download = await downloadsSublevel.get(status.gameId);
|
|
|
|
if (!download) return;
|
|
|
|
const totalSize = await getDirSize(
|
|
path.join(download.downloadPath, status.folderName)
|
|
);
|
|
|
|
if (totalSize < status.fileSize) {
|
|
await this.cancelDownload(status.gameId);
|
|
|
|
await downloadsSublevel.put(status.gameId, {
|
|
...download,
|
|
status: "paused",
|
|
shouldSeed: false,
|
|
progress: totalSize / status.fileSize,
|
|
});
|
|
|
|
WindowManager.mainWindow?.webContents.send("on-hard-delete");
|
|
}
|
|
});
|
|
|
|
WindowManager.mainWindow?.webContents.send("on-seeding-status", seedStatus);
|
|
}
|
|
|
|
static async pauseDownload(downloadKey = this.downloadingGameId) {
|
|
await PythonRPC.rpc
|
|
.post("/action", {
|
|
action: "pause",
|
|
game_id: downloadKey,
|
|
} as PauseDownloadPayload)
|
|
.catch(() => {});
|
|
|
|
WindowManager.mainWindow?.setProgressBar(-1);
|
|
this.downloadingGameId = null;
|
|
}
|
|
|
|
static async resumeDownload(download: Download) {
|
|
return this.startDownload(download);
|
|
}
|
|
|
|
static async cancelDownload(downloadKey = this.downloadingGameId) {
|
|
await PythonRPC.rpc.post("/action", {
|
|
action: "cancel",
|
|
game_id: downloadKey,
|
|
});
|
|
|
|
WindowManager.mainWindow?.setProgressBar(-1);
|
|
|
|
if (downloadKey === this.downloadingGameId) {
|
|
WindowManager.mainWindow?.webContents.send("on-download-progress", null);
|
|
this.downloadingGameId = null;
|
|
}
|
|
}
|
|
|
|
static async resumeSeeding(download: Download) {
|
|
await PythonRPC.rpc.post("/action", {
|
|
action: "resume_seeding",
|
|
game_id: levelKeys.game(download.shop, download.objectId),
|
|
url: download.uri,
|
|
save_path: download.downloadPath,
|
|
});
|
|
}
|
|
|
|
static async pauseSeeding(downloadKey: string) {
|
|
await PythonRPC.rpc.post("/action", {
|
|
action: "pause_seeding",
|
|
game_id: downloadKey,
|
|
});
|
|
}
|
|
|
|
private static async getDownloadPayload(download: Download) {
|
|
const downloadId = levelKeys.game(download.shop, download.objectId);
|
|
|
|
switch (download.downloader) {
|
|
case Downloader.Gofile: {
|
|
const id = download.uri.split("/").pop();
|
|
const token = await GofileApi.authorize();
|
|
const downloadLink = await GofileApi.getDownloadLink(id!);
|
|
|
|
await GofileApi.checkDownloadUrl(downloadLink);
|
|
|
|
return {
|
|
action: "start",
|
|
game_id: downloadId,
|
|
url: downloadLink,
|
|
save_path: download.downloadPath,
|
|
header: `Cookie: accountToken=${token}`,
|
|
};
|
|
}
|
|
case Downloader.PixelDrain: {
|
|
const id = download.uri.split("/").pop();
|
|
|
|
return {
|
|
action: "start",
|
|
game_id: downloadId,
|
|
url: `https://cdn.pd5-gamedriveorg.workers.dev/api/file/${id}`,
|
|
save_path: download.downloadPath,
|
|
};
|
|
}
|
|
case Downloader.Qiwi: {
|
|
const downloadUrl = await QiwiApi.getDownloadUrl(download.uri);
|
|
return {
|
|
action: "start",
|
|
game_id: downloadId,
|
|
url: downloadUrl,
|
|
save_path: download.downloadPath,
|
|
};
|
|
}
|
|
case Downloader.Datanodes: {
|
|
const downloadUrl = await DatanodesApi.getDownloadUrl(download.uri);
|
|
return {
|
|
action: "start",
|
|
game_id: downloadId,
|
|
url: downloadUrl,
|
|
save_path: download.downloadPath,
|
|
};
|
|
}
|
|
case Downloader.Mediafire: {
|
|
const downloadUrl = await MediafireApi.getDownloadUrl(download.uri);
|
|
|
|
return {
|
|
action: "start",
|
|
game_id: downloadId,
|
|
url: downloadUrl,
|
|
save_path: download.downloadPath,
|
|
};
|
|
}
|
|
case Downloader.Torrent:
|
|
return {
|
|
action: "start",
|
|
game_id: downloadId,
|
|
url: download.uri,
|
|
save_path: download.downloadPath,
|
|
};
|
|
case Downloader.RealDebrid: {
|
|
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
|
|
|
|
if (!downloadUrl) throw new Error(DownloadError.NotCachedInRealDebrid);
|
|
|
|
return {
|
|
action: "start",
|
|
game_id: downloadId,
|
|
url: downloadUrl,
|
|
save_path: download.downloadPath,
|
|
};
|
|
}
|
|
case Downloader.TorBox: {
|
|
const { name, url } = await TorBoxClient.getDownloadInfo(download.uri);
|
|
|
|
if (!url) return;
|
|
return {
|
|
action: "start",
|
|
game_id: downloadId,
|
|
url,
|
|
save_path: download.downloadPath,
|
|
out: name,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
static async startDownload(download: Download) {
|
|
const payload = await this.getDownloadPayload(download);
|
|
await PythonRPC.rpc.post("/action", payload);
|
|
this.downloadingGameId = levelKeys.game(download.shop, download.objectId);
|
|
}
|
|
}
|