mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 05:24:55 +03:00
feat: adding aria2c
This commit is contained in:
parent
2d8b63c803
commit
4060f7a1a6
@ -1,4 +1,3 @@
|
|||||||
MAIN_VITE_API_URL=API_URL
|
MAIN_VITE_API_URL=API_URL
|
||||||
MAIN_VITE_AUTH_URL=AUTH_URL
|
MAIN_VITE_AUTH_URL=AUTH_URL
|
||||||
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
|
|
||||||
RENDERER_VITE_INTERCOM_APP_ID=YOUR_APP_ID
|
RENDERER_VITE_INTERCOM_APP_ID=YOUR_APP_ID
|
||||||
|
@ -2,6 +2,7 @@ const { default: axios } = require("axios");
|
|||||||
const util = require("node:util");
|
const util = require("node:util");
|
||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
|
const { spawnSync } = require("node:child_process");
|
||||||
|
|
||||||
const exec = util.promisify(require("node:child_process").exec);
|
const exec = util.promisify(require("node:child_process").exec);
|
||||||
|
|
||||||
@ -46,4 +47,76 @@ const downloadLudusavi = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const downloadAria2WindowsAndLinux = 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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyAria2Macos = async () => {
|
||||||
|
console.log("Checking if aria2 is installed...");
|
||||||
|
|
||||||
|
const isAria2Installed = spawnSync("which", ["aria2c"]).status;
|
||||||
|
|
||||||
|
if (isAria2Installed != 0) {
|
||||||
|
console.log("Please install aria2");
|
||||||
|
console.log("brew install aria2");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Copying aria2 binary...");
|
||||||
|
fs.mkdirSync("aria2");
|
||||||
|
await exec(`cp $(which aria2c) aria2/aria2c`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
fs.copyFileSync(
|
||||||
|
"node_modules/ps-list/vendor/fastlist-0.3.0-x64.exe",
|
||||||
|
"fastlist.exe"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform == "darwin") {
|
||||||
|
copyAria2Macos();
|
||||||
|
} else {
|
||||||
|
downloadAria2WindowsAndLinux();
|
||||||
|
}
|
||||||
|
|
||||||
downloadLudusavi();
|
downloadLudusavi();
|
||||||
|
@ -4,3 +4,5 @@ cx_Logging; sys_platform == 'win32'
|
|||||||
pywin32; sys_platform == 'win32'
|
pywin32; sys_platform == 'win32'
|
||||||
psutil
|
psutil
|
||||||
Pillow
|
Pillow
|
||||||
|
flask
|
||||||
|
aria2p
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import {
|
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
|
||||||
DownloadManager,
|
|
||||||
HydraApi,
|
|
||||||
PythonInstance,
|
|
||||||
gamesPlaytime,
|
|
||||||
} from "@main/services";
|
|
||||||
import { dataSource } from "@main/data-source";
|
import { dataSource } from "@main/data-source";
|
||||||
import { DownloadQueue, Game, UserAuth, UserSubscription } from "@main/entity";
|
import { DownloadQueue, Game, UserAuth, UserSubscription } from "@main/entity";
|
||||||
|
|
||||||
@ -32,7 +27,8 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
DownloadManager.cancelDownload();
|
DownloadManager.cancelDownload();
|
||||||
|
|
||||||
/* Disconnects libtorrent */
|
/* Disconnects libtorrent */
|
||||||
PythonInstance.killTorrent();
|
// TODO
|
||||||
|
// TorrentDownloader.killTorrent();
|
||||||
|
|
||||||
HydraApi.handleSignOut();
|
HydraApi.handleSignOut();
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { PythonInstance, logger } from "@main/services";
|
import { logger } from "@main/services";
|
||||||
import sudo from "sudo-prompt";
|
import sudo from "sudo-prompt";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
|
|
||||||
@ -16,7 +16,8 @@ const closeGame = async (
|
|||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
gameId: number
|
||||||
) => {
|
) => {
|
||||||
const processes = await PythonInstance.getProcessList();
|
// const processes = await PythonInstance.getProcessList();
|
||||||
|
const processes = [];
|
||||||
const game = await gameRepository.findOne({
|
const game = await gameRepository.findOne({
|
||||||
where: { id: gameId, isDeleted: false },
|
where: { id: gameId, isDeleted: false },
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { PythonInstance } from "@main/services";
|
|
||||||
|
|
||||||
const processProfileImage = async (
|
const processProfileImage = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
path: string
|
path: string
|
||||||
) => {
|
) => {
|
||||||
return PythonInstance.processProfileImage(path);
|
return path;
|
||||||
|
// return PythonInstance.processProfileImage(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("processProfileImage", processProfileImage);
|
registerEvent("processProfileImage", processProfileImage);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { RealDebridClient } from "@main/services/real-debrid";
|
import { RealDebridClient } from "@main/services/download/real-debrid";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
const authenticateRealDebrid = async (
|
const authenticateRealDebrid = async (
|
||||||
|
@ -5,12 +5,14 @@ import path from "node:path";
|
|||||||
import url from "node:url";
|
import url from "node:url";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||||
import { logger, PythonInstance, WindowManager } from "@main/services";
|
import { logger, WindowManager } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { dataSource } from "@main/data-source";
|
||||||
import resources from "@locales";
|
import resources from "@locales";
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
import { userPreferencesRepository } from "@main/repository";
|
||||||
import { knexClient, migrationConfig } from "./knex-client";
|
import { knexClient, migrationConfig } from "./knex-client";
|
||||||
import { databaseDirectory } from "./constants";
|
import { databaseDirectory } from "./constants";
|
||||||
|
import { PythonRPC } from "./services/python-rpc";
|
||||||
|
import { Aria2 } from "./services/aria2";
|
||||||
|
|
||||||
const { autoUpdater } = updater;
|
const { autoUpdater } = updater;
|
||||||
|
|
||||||
@ -146,7 +148,8 @@ app.on("window-all-closed", () => {
|
|||||||
|
|
||||||
app.on("before-quit", () => {
|
app.on("before-quit", () => {
|
||||||
/* Disconnects libtorrent */
|
/* Disconnects libtorrent */
|
||||||
PythonInstance.kill();
|
PythonRPC.kill();
|
||||||
|
Aria2.kill();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
|
@ -1,21 +1,20 @@
|
|||||||
import {
|
import { DownloadManager, Ludusavi, startMainLoop } from "./services";
|
||||||
DownloadManager,
|
|
||||||
Ludusavi,
|
|
||||||
PythonInstance,
|
|
||||||
startMainLoop,
|
|
||||||
} from "./services";
|
|
||||||
import {
|
import {
|
||||||
downloadQueueRepository,
|
downloadQueueRepository,
|
||||||
userPreferencesRepository,
|
userPreferencesRepository,
|
||||||
} from "./repository";
|
} from "./repository";
|
||||||
import { UserPreferences } from "./entity";
|
import { UserPreferences } from "./entity";
|
||||||
import { RealDebridClient } from "./services/real-debrid";
|
import { RealDebridClient } from "./services/download/real-debrid";
|
||||||
import { HydraApi } from "./services/hydra-api";
|
import { HydraApi } from "./services/hydra-api";
|
||||||
import { uploadGamesBatch } from "./services/library-sync";
|
import { uploadGamesBatch } from "./services/library-sync";
|
||||||
|
import { PythonRPC } from "./services/python-rpc";
|
||||||
|
import { Aria2 } from "./services/aria2";
|
||||||
|
|
||||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
const loadState = async (userPreferences: UserPreferences | null) => {
|
||||||
import("./events");
|
import("./events");
|
||||||
|
|
||||||
|
Aria2.spawn();
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
if (userPreferences?.realDebridApiToken) {
|
||||||
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
||||||
}
|
}
|
||||||
@ -35,11 +34,14 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (nextQueueItem?.game.status === "active") {
|
PythonRPC.spawn();
|
||||||
DownloadManager.startDownload(nextQueueItem.game);
|
// start download
|
||||||
} else {
|
|
||||||
PythonInstance.spawn();
|
// if (nextQueueItem?.game.status === "active") {
|
||||||
}
|
// DownloadManager.startDownload(nextQueueItem.game);
|
||||||
|
// } else {
|
||||||
|
// PythonInstance.spawn();
|
||||||
|
// }
|
||||||
|
|
||||||
startMainLoop();
|
startMainLoop();
|
||||||
};
|
};
|
||||||
|
@ -1,42 +1,120 @@
|
|||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
import { PythonInstance } from "./python-instance";
|
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import { downloadQueueRepository, gameRepository } from "@main/repository";
|
import {
|
||||||
|
downloadQueueRepository,
|
||||||
|
gameRepository,
|
||||||
|
userPreferencesRepository,
|
||||||
|
} from "@main/repository";
|
||||||
import { publishDownloadCompleteNotification } from "../notifications";
|
import { publishDownloadCompleteNotification } from "../notifications";
|
||||||
import { RealDebridDownloader } from "./real-debrid-downloader";
|
|
||||||
import type { DownloadProgress } from "@types";
|
import type { DownloadProgress } from "@types";
|
||||||
import { GofileApi, QiwiApi } from "../hosters";
|
import { GofileApi, QiwiApi } from "../hosters";
|
||||||
import { GenericHttpDownloader } from "./generic-http-downloader";
|
import { PythonRPC } from "../python-rpc";
|
||||||
import { In, Not } from "typeorm";
|
import {
|
||||||
import path from "path";
|
LibtorrentPayload,
|
||||||
import fs from "fs";
|
LibtorrentStatus,
|
||||||
|
PauseDownloadPayload,
|
||||||
|
} from "./types";
|
||||||
|
import { calculateETA } from "./helpers";
|
||||||
|
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||||
|
import { RealDebridClient } from "./real-debrid";
|
||||||
|
|
||||||
export class DownloadManager {
|
export class DownloadManager {
|
||||||
private static currentDownloader: Downloader | null = null;
|
|
||||||
private static downloadingGameId: number | null = null;
|
private static downloadingGameId: number | null = null;
|
||||||
|
|
||||||
public static async watchDownloads() {
|
private static async getDownloadStatus() {
|
||||||
let status: DownloadProgress | null = null;
|
const response = await PythonRPC.rpc.get<LibtorrentPayload | null>(
|
||||||
|
"/status"
|
||||||
|
);
|
||||||
|
|
||||||
if (this.currentDownloader === Downloader.Torrent) {
|
if (response.data === null || !this.downloadingGameId) return null;
|
||||||
status = await PythonInstance.getStatus();
|
|
||||||
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
const gameId = this.downloadingGameId;
|
||||||
status = await RealDebridDownloader.getStatus();
|
|
||||||
} else {
|
try {
|
||||||
status = await GenericHttpDownloader.getStatus();
|
const {
|
||||||
|
progress,
|
||||||
|
numPeers,
|
||||||
|
numSeeds,
|
||||||
|
downloadSpeed,
|
||||||
|
bytesDownloaded,
|
||||||
|
fileSize,
|
||||||
|
folderName,
|
||||||
|
status,
|
||||||
|
} = response.data;
|
||||||
|
|
||||||
|
const isDownloadingMetadata =
|
||||||
|
status === LibtorrentStatus.DownloadingMetadata;
|
||||||
|
|
||||||
|
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
|
||||||
|
|
||||||
|
if (!isDownloadingMetadata && !isCheckingFiles) {
|
||||||
|
const update: QueryDeepPartialEntity<Game> = {
|
||||||
|
bytesDownloaded,
|
||||||
|
fileSize,
|
||||||
|
progress,
|
||||||
|
status: "active",
|
||||||
|
};
|
||||||
|
|
||||||
|
await gameRepository.update(
|
||||||
|
{ id: gameId },
|
||||||
|
{
|
||||||
|
...update,
|
||||||
|
folderName,
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress === 1 && !isCheckingFiles) {
|
||||||
|
const userPreferences = await userPreferencesRepository.findOneBy({
|
||||||
|
id: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userPreferences?.seedAfterDownloadComplete) {
|
||||||
|
gameRepository.update(
|
||||||
|
{ id: gameId },
|
||||||
|
{ status: "seeding", shouldSeed: true }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
gameRepository.update(
|
||||||
|
{ id: gameId },
|
||||||
|
{ status: "complete", shouldSeed: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.pauseSeeding(gameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.downloadingGameId = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
numPeers,
|
||||||
|
numSeeds,
|
||||||
|
downloadSpeed,
|
||||||
|
timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed),
|
||||||
|
isDownloadingMetadata,
|
||||||
|
isCheckingFiles,
|
||||||
|
progress,
|
||||||
|
gameId,
|
||||||
|
} as DownloadProgress;
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async watchDownloads() {
|
||||||
|
const status = await this.getDownloadStatus();
|
||||||
|
|
||||||
|
// // status = await RealDebridDownloader.getStatus();
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
const { gameId, progress } = status;
|
const { gameId, progress } = status;
|
||||||
|
|
||||||
const game = await gameRepository.findOne({
|
const game = await gameRepository.findOne({
|
||||||
where: { id: gameId, isDeleted: false },
|
where: { id: gameId, isDeleted: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (WindowManager.mainWindow && game) {
|
if (WindowManager.mainWindow && game) {
|
||||||
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
||||||
|
|
||||||
WindowManager.mainWindow.webContents.send(
|
WindowManager.mainWindow.webContents.send(
|
||||||
"on-download-progress",
|
"on-download-progress",
|
||||||
JSON.parse(
|
JSON.parse(
|
||||||
@ -47,12 +125,9 @@ export class DownloadManager {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progress === 1 && game) {
|
if (progress === 1 && game) {
|
||||||
publishDownloadCompleteNotification(game);
|
publishDownloadCompleteNotification(game);
|
||||||
|
|
||||||
await downloadQueueRepository.delete({ game });
|
await downloadQueueRepository.delete({ game });
|
||||||
|
|
||||||
const [nextQueueItem] = await downloadQueueRepository.find({
|
const [nextQueueItem] = await downloadQueueRepository.find({
|
||||||
order: {
|
order: {
|
||||||
id: "DESC",
|
id: "DESC",
|
||||||
@ -61,7 +136,6 @@ export class DownloadManager {
|
|||||||
game: true,
|
game: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (nextQueueItem) {
|
if (nextQueueItem) {
|
||||||
this.resumeDownload(nextQueueItem.game);
|
this.resumeDownload(nextQueueItem.game);
|
||||||
}
|
}
|
||||||
@ -70,88 +144,80 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async getSeedStatus() {
|
public static async getSeedStatus() {
|
||||||
const gamesToSeed = await gameRepository.find({
|
// const gamesToSeed = await gameRepository.find({
|
||||||
where: { shouldSeed: true, isDeleted: false },
|
// where: { shouldSeed: true, isDeleted: false },
|
||||||
});
|
// });
|
||||||
|
// if (gamesToSeed.length === 0) return;
|
||||||
if (gamesToSeed.length === 0) return;
|
// const seedStatus = await PythonRPC.rpc
|
||||||
|
// .get<LibtorrentPayload[] | null>("/seed-status")
|
||||||
const seedStatus = await PythonInstance.getSeedStatus();
|
// .then((results) => {
|
||||||
|
// if (results === null) return [];
|
||||||
if (seedStatus.length === 0) {
|
// return results.data;
|
||||||
for (const game of gamesToSeed) {
|
// });
|
||||||
if (game.uri && game.downloadPath) {
|
// if (!seedStatus.length === 0) {
|
||||||
await this.resumeSeeding(game.id, game.uri, game.downloadPath);
|
// for (const game of gamesToSeed) {
|
||||||
}
|
// if (game.uri && game.downloadPath) {
|
||||||
}
|
// await this.resumeSeeding(game.id, game.uri, game.downloadPath);
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
const gameIds = seedStatus.map((status) => status.gameId);
|
// }
|
||||||
|
// const gameIds = seedStatus.map((status) => status.gameId);
|
||||||
for (const gameId of gameIds) {
|
// for (const gameId of gameIds) {
|
||||||
const game = await gameRepository.findOne({
|
// const game = await gameRepository.findOne({
|
||||||
where: { id: gameId },
|
// where: { id: gameId },
|
||||||
});
|
// });
|
||||||
|
// if (game) {
|
||||||
if (game) {
|
// const isNotDeleted = fs.existsSync(
|
||||||
const isNotDeleted = fs.existsSync(
|
// path.join(game.downloadPath!, game.folderName!)
|
||||||
path.join(game.downloadPath!, game.folderName!)
|
// );
|
||||||
);
|
// if (!isNotDeleted) {
|
||||||
|
// await this.pauseSeeding(game.id);
|
||||||
if (!isNotDeleted) {
|
// await gameRepository.update(game.id, {
|
||||||
await this.pauseSeeding(game.id);
|
// status: "complete",
|
||||||
|
// shouldSeed: false,
|
||||||
await gameRepository.update(game.id, {
|
// });
|
||||||
status: "complete",
|
// WindowManager.mainWindow?.webContents.send("on-hard-delete");
|
||||||
shouldSeed: false,
|
// }
|
||||||
});
|
// }
|
||||||
|
// }
|
||||||
WindowManager.mainWindow?.webContents.send("on-hard-delete");
|
// const updateList = await gameRepository.find({
|
||||||
}
|
// where: {
|
||||||
}
|
// id: In(gameIds),
|
||||||
}
|
// status: Not(In(["complete", "seeding"])),
|
||||||
|
// shouldSeed: true,
|
||||||
const updateList = await gameRepository.find({
|
// isDeleted: false,
|
||||||
where: {
|
// },
|
||||||
id: In(gameIds),
|
// });
|
||||||
status: Not(In(["complete", "seeding"])),
|
// if (updateList.length > 0) {
|
||||||
shouldSeed: true,
|
// await gameRepository.update(
|
||||||
isDeleted: false,
|
// { id: In(updateList.map((game) => game.id)) },
|
||||||
},
|
// { status: "seeding" }
|
||||||
});
|
// );
|
||||||
|
// }
|
||||||
if (updateList.length > 0) {
|
// WindowManager.mainWindow?.webContents.send(
|
||||||
await gameRepository.update(
|
// "on-seeding-status",
|
||||||
{ id: In(updateList.map((game) => game.id)) },
|
// JSON.parse(JSON.stringify(seedStatus))
|
||||||
{ status: "seeding" }
|
// );
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowManager.mainWindow?.webContents.send(
|
|
||||||
"on-seeding-status",
|
|
||||||
JSON.parse(JSON.stringify(seedStatus))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async pauseSeeding(gameId: number) {
|
static async pauseSeeding(gameId: number) {
|
||||||
await PythonInstance.pauseSeeding(gameId);
|
// await TorrentDownloader.pauseSeeding(gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resumeSeeding(gameId: number, magnet: string, savePath: string) {
|
static async resumeSeeding(gameId: number, magnet: string, savePath: string) {
|
||||||
await PythonInstance.resumeSeeding(gameId, magnet, savePath);
|
// await TorrentDownloader.resumeSeeding(gameId, magnet, savePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async pauseDownload() {
|
static async pauseDownload() {
|
||||||
if (this.currentDownloader === Downloader.Torrent) {
|
await PythonRPC.rpc
|
||||||
await PythonInstance.pauseDownload();
|
.post("/action", {
|
||||||
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
action: "pause",
|
||||||
await RealDebridDownloader.pauseDownload();
|
game_id: this.downloadingGameId,
|
||||||
} else {
|
} as PauseDownloadPayload)
|
||||||
await GenericHttpDownloader.pauseDownload();
|
.catch(() => {});
|
||||||
}
|
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
this.currentDownloader = null;
|
|
||||||
this.downloadingGameId = null;
|
this.downloadingGameId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,16 +226,13 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async cancelDownload(gameId = this.downloadingGameId!) {
|
static async cancelDownload(gameId = this.downloadingGameId!) {
|
||||||
if (this.currentDownloader === Downloader.Torrent) {
|
await PythonRPC.rpc.post("/action", {
|
||||||
PythonInstance.cancelDownload(gameId);
|
action: "cancel",
|
||||||
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
game_id: gameId,
|
||||||
RealDebridDownloader.cancelDownload(gameId);
|
});
|
||||||
} else {
|
|
||||||
GenericHttpDownloader.cancelDownload(gameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
this.currentDownloader = null;
|
|
||||||
this.downloadingGameId = null;
|
this.downloadingGameId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,34 +244,57 @@ export class DownloadManager {
|
|||||||
const token = await GofileApi.authorize();
|
const token = await GofileApi.authorize();
|
||||||
const downloadLink = await GofileApi.getDownloadLink(id!);
|
const downloadLink = await GofileApi.getDownloadLink(id!);
|
||||||
|
|
||||||
GenericHttpDownloader.startDownload(game, downloadLink, {
|
await PythonRPC.rpc.post("/action", {
|
||||||
Cookie: `accountToken=${token}`,
|
action: "start",
|
||||||
|
game_id: game.id,
|
||||||
|
url: downloadLink,
|
||||||
|
save_path: game.downloadPath,
|
||||||
|
header: `Cookie: accountToken=${token}`,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Downloader.PixelDrain: {
|
case Downloader.PixelDrain: {
|
||||||
const id = game!.uri!.split("/").pop();
|
const id = game!.uri!.split("/").pop();
|
||||||
|
|
||||||
await GenericHttpDownloader.startDownload(
|
await PythonRPC.rpc.post("/action", {
|
||||||
game,
|
action: "start",
|
||||||
`https://pixeldrain.com/api/file/${id}?download`
|
game_id: game.id,
|
||||||
);
|
url: `https://pixeldrain.com/api/file/${id}?download`,
|
||||||
|
save_path: game.downloadPath,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Downloader.Qiwi: {
|
case Downloader.Qiwi: {
|
||||||
const downloadUrl = await QiwiApi.getDownloadUrl(game.uri!);
|
const downloadUrl = await QiwiApi.getDownloadUrl(game.uri!);
|
||||||
|
|
||||||
await GenericHttpDownloader.startDownload(game, downloadUrl);
|
await PythonRPC.rpc.post("/action", {
|
||||||
|
action: "start",
|
||||||
|
game_id: game.id,
|
||||||
|
url: downloadUrl,
|
||||||
|
save_path: game.downloadPath,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Downloader.Torrent:
|
case Downloader.Torrent:
|
||||||
PythonInstance.startDownload(game);
|
await PythonRPC.rpc.post("/action", {
|
||||||
|
action: "start",
|
||||||
|
game_id: game.id,
|
||||||
|
url: game.uri,
|
||||||
|
save_path: game.downloadPath,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case Downloader.RealDebrid:
|
case Downloader.RealDebrid: {
|
||||||
RealDebridDownloader.startDownload(game);
|
const downloadUrl = await RealDebridClient.getDownloadUrl(game.uri!);
|
||||||
|
|
||||||
|
await PythonRPC.rpc.post("/action", {
|
||||||
|
action: "start",
|
||||||
|
game_id: game.id,
|
||||||
|
url: downloadUrl,
|
||||||
|
save_path: game.downloadPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentDownloader = game.downloader;
|
|
||||||
this.downloadingGameId = game.id;
|
this.downloadingGameId = game.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,109 +0,0 @@
|
|||||||
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 {
|
|
||||||
public static downloads = new Map<number, HttpDownload>();
|
|
||||||
public static downloadingGame: Game | null = null;
|
|
||||||
|
|
||||||
public static async getStatus() {
|
|
||||||
if (this.downloadingGame) {
|
|
||||||
const download = this.downloads.get(this.downloadingGame.id)!;
|
|
||||||
const status = download.getStatus();
|
|
||||||
|
|
||||||
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 httpDownload = this.downloads.get(this.downloadingGame!.id!);
|
|
||||||
|
|
||||||
if (httpDownload) {
|
|
||||||
await httpDownload.pauseDownload();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.downloadingGame = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async startDownload(
|
|
||||||
game: Game,
|
|
||||||
downloadUrl: string,
|
|
||||||
headers?: Record<string, string>
|
|
||||||
) {
|
|
||||||
this.downloadingGame = game;
|
|
||||||
|
|
||||||
if (this.downloads.has(game.id)) {
|
|
||||||
await this.resumeDownload(game.id!);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const httpDownload = new HttpDownload(
|
|
||||||
game.downloadPath!,
|
|
||||||
downloadUrl,
|
|
||||||
headers
|
|
||||||
);
|
|
||||||
|
|
||||||
httpDownload.startDownload();
|
|
||||||
|
|
||||||
this.downloads.set(game.id!, httpDownload);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async cancelDownload(gameId: number) {
|
|
||||||
const httpDownload = this.downloads.get(gameId);
|
|
||||||
|
|
||||||
if (httpDownload) {
|
|
||||||
await httpDownload.cancelDownload();
|
|
||||||
this.downloads.delete(gameId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async resumeDownload(gameId: number) {
|
|
||||||
const httpDownload = this.downloads.get(gameId);
|
|
||||||
|
|
||||||
if (httpDownload) {
|
|
||||||
await httpDownload.resumeDownload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
import { WindowManager } from "../window-manager";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
export class HttpDownload {
|
|
||||||
private downloadItem: Electron.DownloadItem;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private downloadPath: string,
|
|
||||||
private downloadUrl: string,
|
|
||||||
private headers?: Record<string, string>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public getStatus() {
|
|
||||||
return {
|
|
||||||
completedLength: this.downloadItem.getReceivedBytes(),
|
|
||||||
totalLength: this.downloadItem.getTotalBytes(),
|
|
||||||
downloadSpeed: this.downloadItem.getCurrentBytesPerSecond(),
|
|
||||||
folderName: this.downloadItem.getFilename(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelDownload() {
|
|
||||||
this.downloadItem.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
async pauseDownload() {
|
|
||||||
this.downloadItem.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
async resumeDownload() {
|
|
||||||
this.downloadItem.resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
async startDownload() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const options = this.headers ? { headers: this.headers } : {};
|
|
||||||
WindowManager.mainWindow?.webContents.downloadURL(
|
|
||||||
this.downloadUrl,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
WindowManager.mainWindow?.webContents.session.once(
|
|
||||||
"will-download",
|
|
||||||
(_event, item, _webContents) => {
|
|
||||||
this.downloadItem = item;
|
|
||||||
|
|
||||||
item.setSavePath(path.join(this.downloadPath, item.getFilename()));
|
|
||||||
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,2 +1 @@
|
|||||||
export * from "./download-manager";
|
export * from "./download-manager";
|
||||||
export * from "./python-instance";
|
|
||||||
|
@ -1,236 +0,0 @@
|
|||||||
import cp from "node:child_process";
|
|
||||||
|
|
||||||
import { Game } from "@main/entity";
|
|
||||||
import {
|
|
||||||
RPC_PASSWORD,
|
|
||||||
RPC_PORT,
|
|
||||||
startTorrentClient as startRPCClient,
|
|
||||||
} from "./torrent-client";
|
|
||||||
import { gameRepository, userPreferencesRepository } from "@main/repository";
|
|
||||||
import type { DownloadProgress } from "@types";
|
|
||||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
|
||||||
import { calculateETA } from "./helpers";
|
|
||||||
import axios from "axios";
|
|
||||||
import {
|
|
||||||
CancelDownloadPayload,
|
|
||||||
StartDownloadPayload,
|
|
||||||
PauseDownloadPayload,
|
|
||||||
LibtorrentStatus,
|
|
||||||
LibtorrentPayload,
|
|
||||||
ProcessPayload,
|
|
||||||
PauseSeedingPayload,
|
|
||||||
ResumeSeedingPayload,
|
|
||||||
} from "./types";
|
|
||||||
import { pythonInstanceLogger as logger } from "../logger";
|
|
||||||
|
|
||||||
export class PythonInstance {
|
|
||||||
private static pythonProcess: cp.ChildProcess | null = null;
|
|
||||||
private static downloadingGameId = -1;
|
|
||||||
|
|
||||||
private static rpc = axios.create({
|
|
||||||
baseURL: `http://localhost:${RPC_PORT}`,
|
|
||||||
headers: {
|
|
||||||
"x-hydra-rpc-password": RPC_PASSWORD,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
public static spawn(args?: StartDownloadPayload) {
|
|
||||||
logger.log("spawning python process with args:", args);
|
|
||||||
this.pythonProcess = startRPCClient(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static kill() {
|
|
||||||
if (this.pythonProcess) {
|
|
||||||
logger.log("killing python process");
|
|
||||||
this.pythonProcess.kill();
|
|
||||||
this.pythonProcess = null;
|
|
||||||
this.downloadingGameId = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static killTorrent() {
|
|
||||||
if (this.pythonProcess) {
|
|
||||||
logger.log("killing torrent in python process");
|
|
||||||
this.rpc.post("/action", { action: "kill-torrent" });
|
|
||||||
this.downloadingGameId = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async getProcessList() {
|
|
||||||
return (
|
|
||||||
(await this.rpc.get<ProcessPayload[] | null>("/process-list")).data || []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async getStatus() {
|
|
||||||
if (this.downloadingGameId === -1) return null;
|
|
||||||
|
|
||||||
const response = await this.rpc.get<LibtorrentPayload | null>("/status");
|
|
||||||
|
|
||||||
if (response.data === null) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
progress,
|
|
||||||
numPeers,
|
|
||||||
numSeeds,
|
|
||||||
downloadSpeed,
|
|
||||||
bytesDownloaded,
|
|
||||||
fileSize,
|
|
||||||
folderName,
|
|
||||||
status,
|
|
||||||
gameId,
|
|
||||||
} = response.data;
|
|
||||||
|
|
||||||
this.downloadingGameId = gameId;
|
|
||||||
|
|
||||||
const isDownloadingMetadata =
|
|
||||||
status === LibtorrentStatus.DownloadingMetadata;
|
|
||||||
|
|
||||||
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
|
|
||||||
|
|
||||||
if (!isDownloadingMetadata && !isCheckingFiles) {
|
|
||||||
const update: QueryDeepPartialEntity<Game> = {
|
|
||||||
bytesDownloaded,
|
|
||||||
fileSize,
|
|
||||||
progress,
|
|
||||||
status: "active",
|
|
||||||
};
|
|
||||||
|
|
||||||
await gameRepository.update(
|
|
||||||
{ id: gameId },
|
|
||||||
{
|
|
||||||
...update,
|
|
||||||
folderName,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress === 1 && !isCheckingFiles) {
|
|
||||||
const userPreferences = await userPreferencesRepository.findOneBy({
|
|
||||||
id: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userPreferences?.seedAfterDownloadComplete) {
|
|
||||||
gameRepository.update(
|
|
||||||
{ id: gameId },
|
|
||||||
{ status: "seeding", shouldSeed: true }
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
gameRepository.update(
|
|
||||||
{ id: gameId },
|
|
||||||
{ status: "complete", shouldSeed: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
this.pauseSeeding(gameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.downloadingGameId = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
numPeers,
|
|
||||||
numSeeds,
|
|
||||||
downloadSpeed,
|
|
||||||
timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed),
|
|
||||||
isDownloadingMetadata,
|
|
||||||
isCheckingFiles,
|
|
||||||
progress,
|
|
||||||
gameId,
|
|
||||||
} as DownloadProgress;
|
|
||||||
} catch (err) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async getSeedStatus() {
|
|
||||||
const response = await this.rpc.get<LibtorrentPayload[] | null>(
|
|
||||||
"/seed-status"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data === null) return [];
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async pauseSeeding(gameId: number) {
|
|
||||||
await this.rpc
|
|
||||||
.post("/action", {
|
|
||||||
action: "pause-seeding",
|
|
||||||
game_id: gameId,
|
|
||||||
} as PauseSeedingPayload)
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async resumeSeeding(gameId: number, magnet: string, savePath: string) {
|
|
||||||
await this.rpc
|
|
||||||
.post("/action", {
|
|
||||||
action: "resume-seeding",
|
|
||||||
game_id: gameId,
|
|
||||||
magnet,
|
|
||||||
save_path: savePath,
|
|
||||||
} as ResumeSeedingPayload)
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async pauseDownload() {
|
|
||||||
await this.rpc
|
|
||||||
.post("/action", {
|
|
||||||
action: "pause",
|
|
||||||
game_id: this.downloadingGameId,
|
|
||||||
} as PauseDownloadPayload)
|
|
||||||
.catch(() => {});
|
|
||||||
|
|
||||||
this.downloadingGameId = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async startDownload(game: Game) {
|
|
||||||
if (!this.pythonProcess) {
|
|
||||||
this.spawn({
|
|
||||||
game_id: game.id,
|
|
||||||
magnet: game.uri!,
|
|
||||||
save_path: game.downloadPath!,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await this.rpc
|
|
||||||
.post("/action", {
|
|
||||||
action: "start",
|
|
||||||
game_id: game.id,
|
|
||||||
magnet: game.uri,
|
|
||||||
save_path: game.downloadPath,
|
|
||||||
} as StartDownloadPayload)
|
|
||||||
.catch(this.handleRpcError);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.downloadingGameId = game.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async cancelDownload(gameId: number) {
|
|
||||||
await this.rpc
|
|
||||||
.post("/action", {
|
|
||||||
action: "cancel",
|
|
||||||
game_id: gameId,
|
|
||||||
} as CancelDownloadPayload)
|
|
||||||
.catch(() => {});
|
|
||||||
|
|
||||||
this.downloadingGameId = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async processProfileImage(imagePath: string) {
|
|
||||||
return this.rpc
|
|
||||||
.post<{ imagePath: string; mimeType: string }>("/profile-image", {
|
|
||||||
image_path: imagePath,
|
|
||||||
})
|
|
||||||
.then((response) => response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async handleRpcError(_error: unknown) {
|
|
||||||
await this.rpc.get("/healthcheck").catch(() => {
|
|
||||||
logger.error(
|
|
||||||
"RPC healthcheck failed. Killing process and starting again"
|
|
||||||
);
|
|
||||||
this.kill();
|
|
||||||
this.spawn();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
import { Game } from "@main/entity";
|
|
||||||
import { RealDebridClient } from "../real-debrid";
|
|
||||||
import { HttpDownload } from "./http-download";
|
|
||||||
import { GenericHttpDownloader } from "./generic-http-downloader";
|
|
||||||
|
|
||||||
export class RealDebridDownloader extends GenericHttpDownloader {
|
|
||||||
private static realDebridTorrentId: string | null = null;
|
|
||||||
|
|
||||||
private static async getRealDebridDownloadUrl() {
|
|
||||||
if (this.realDebridTorrentId) {
|
|
||||||
let torrentInfo = await RealDebridClient.getTorrentInfo(
|
|
||||||
this.realDebridTorrentId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (torrentInfo.status === "waiting_files_selection") {
|
|
||||||
await RealDebridClient.selectAllFiles(this.realDebridTorrentId);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async startDownload(game: Game) {
|
|
||||||
if (this.downloads.has(game.id)) {
|
|
||||||
await this.resumeDownload(game.id!);
|
|
||||||
this.downloadingGame = game;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game.uri?.startsWith("magnet:")) {
|
|
||||||
this.realDebridTorrentId = await RealDebridClient.getTorrentId(
|
|
||||||
game!.uri!
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.downloadingGame = game;
|
|
||||||
|
|
||||||
const downloadUrl = await this.getRealDebridDownloadUrl();
|
|
||||||
|
|
||||||
if (downloadUrl) {
|
|
||||||
this.realDebridTorrentId = null;
|
|
||||||
|
|
||||||
const httpDownload = new HttpDownload(game.downloadPath!, downloadUrl);
|
|
||||||
httpDownload.startDownload();
|
|
||||||
|
|
||||||
this.downloads.set(game.id!, httpDownload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import cp from "node:child_process";
|
|
||||||
import crypto from "node:crypto";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import { app, dialog } from "electron";
|
|
||||||
import type { StartDownloadPayload } from "./types";
|
|
||||||
import { Readable } from "node:stream";
|
|
||||||
import { pythonInstanceLogger as logger } from "../logger";
|
|
||||||
|
|
||||||
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
|
||||||
darwin: "hydra-download-manager",
|
|
||||||
linux: "hydra-download-manager",
|
|
||||||
win32: "hydra-download-manager.exe",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BITTORRENT_PORT = "5881";
|
|
||||||
export const RPC_PORT = "8084";
|
|
||||||
export const RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
|
|
||||||
|
|
||||||
const logStderr = (readable: Readable | null) => {
|
|
||||||
if (!readable) return;
|
|
||||||
|
|
||||||
readable.setEncoding("utf-8");
|
|
||||||
readable.on("data", logger.log);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const startTorrentClient = (args?: StartDownloadPayload) => {
|
|
||||||
const commonArgs = [
|
|
||||||
BITTORRENT_PORT,
|
|
||||||
RPC_PORT,
|
|
||||||
RPC_PASSWORD,
|
|
||||||
args ? encodeURIComponent(JSON.stringify(args)) : "",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (app.isPackaged) {
|
|
||||||
const binaryName = binaryNameByPlatform[process.platform]!;
|
|
||||||
const binaryPath = path.join(
|
|
||||||
process.resourcesPath,
|
|
||||||
"hydra-download-manager",
|
|
||||||
binaryName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fs.existsSync(binaryPath)) {
|
|
||||||
dialog.showErrorBox(
|
|
||||||
"Fatal",
|
|
||||||
"Hydra Download Manager binary not found. Please check if it has been removed by Windows Defender."
|
|
||||||
);
|
|
||||||
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
|
|
||||||
const childProcess = cp.spawn(binaryPath, commonArgs, {
|
|
||||||
windowsHide: true,
|
|
||||||
stdio: ["inherit", "inherit"],
|
|
||||||
});
|
|
||||||
|
|
||||||
logStderr(childProcess.stderr);
|
|
||||||
|
|
||||||
return childProcess;
|
|
||||||
} else {
|
|
||||||
const scriptPath = path.join(
|
|
||||||
__dirname,
|
|
||||||
"..",
|
|
||||||
"..",
|
|
||||||
"torrent-client",
|
|
||||||
"main.py"
|
|
||||||
);
|
|
||||||
|
|
||||||
const childProcess = cp.spawn("python3", [scriptPath, ...commonArgs], {
|
|
||||||
stdio: ["inherit", "inherit"],
|
|
||||||
});
|
|
||||||
|
|
||||||
logStderr(childProcess.stderr);
|
|
||||||
|
|
||||||
return childProcess;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,9 +1,3 @@
|
|||||||
export interface StartDownloadPayload {
|
|
||||||
game_id: number;
|
|
||||||
magnet: string;
|
|
||||||
save_path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PauseDownloadPayload {
|
export interface PauseDownloadPayload {
|
||||||
game_id: number;
|
game_id: number;
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export class HydraApi {
|
|||||||
private static instance: AxiosInstance;
|
private static instance: AxiosInstance;
|
||||||
|
|
||||||
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
||||||
private static readonly ADD_LOG_INTERCEPTOR = true;
|
private static readonly ADD_LOG_INTERCEPTOR = false;
|
||||||
|
|
||||||
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
|
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
export * from "./logger";
|
export * from "./logger";
|
||||||
export * from "./steam";
|
export * from "./steam";
|
||||||
export * from "./steam-250";
|
export * from "./steam-250";
|
||||||
export * from "./steam-grid";
|
|
||||||
export * from "./window-manager";
|
export * from "./window-manager";
|
||||||
export * from "./download";
|
export * from "./download";
|
||||||
export * from "./process-watcher";
|
export * from "./process-watcher";
|
||||||
|
@ -3,7 +3,7 @@ import { gameRepository } from "@main/repository";
|
|||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import { createGame, updateGamePlaytime } from "./library-sync";
|
import { createGame, updateGamePlaytime } from "./library-sync";
|
||||||
import type { GameRunning } from "@types";
|
import type { GameRunning } from "@types";
|
||||||
import { PythonInstance } from "./download";
|
// import { PythonInstance } from "./download";
|
||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
|
|
||||||
export const gamesPlaytime = new Map<
|
export const gamesPlaytime = new Map<
|
||||||
@ -14,69 +14,7 @@ export const gamesPlaytime = new Map<
|
|||||||
const TICKS_TO_UPDATE_API = 120;
|
const TICKS_TO_UPDATE_API = 120;
|
||||||
let currentTick = 1;
|
let currentTick = 1;
|
||||||
|
|
||||||
export const watchProcesses = async () => {
|
const onGameTick = (game: Game) => {
|
||||||
const games = await gameRepository.find({
|
|
||||||
where: {
|
|
||||||
executablePath: Not(IsNull()),
|
|
||||||
isDeleted: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (games.length === 0) return;
|
|
||||||
const processes = await PythonInstance.getProcessList();
|
|
||||||
|
|
||||||
const processSet = new Set(processes.map((process) => process.exe));
|
|
||||||
|
|
||||||
for (const game of games) {
|
|
||||||
const executablePath = game.executablePath!;
|
|
||||||
|
|
||||||
const gameProcess = processSet.has(executablePath);
|
|
||||||
|
|
||||||
if (gameProcess) {
|
|
||||||
if (gamesPlaytime.has(game.id)) {
|
|
||||||
onTickGame(game);
|
|
||||||
} else {
|
|
||||||
onOpenGame(game);
|
|
||||||
}
|
|
||||||
} else if (gamesPlaytime.has(game.id)) {
|
|
||||||
onCloseGame(game);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentTick++;
|
|
||||||
|
|
||||||
if (WindowManager.mainWindow) {
|
|
||||||
const gamesRunning = Array.from(gamesPlaytime.entries()).map((entry) => {
|
|
||||||
return {
|
|
||||||
id: entry[0],
|
|
||||||
sessionDurationInMillis: performance.now() - entry[1].firstTick,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
WindowManager.mainWindow.webContents.send(
|
|
||||||
"on-games-running",
|
|
||||||
gamesRunning as Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function onOpenGame(game: Game) {
|
|
||||||
const now = performance.now();
|
|
||||||
|
|
||||||
gamesPlaytime.set(game.id, {
|
|
||||||
lastTick: now,
|
|
||||||
firstTick: now,
|
|
||||||
lastSyncTick: now,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (game.remoteId) {
|
|
||||||
updateGamePlaytime(game, 0, new Date()).catch(() => {});
|
|
||||||
} else {
|
|
||||||
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTickGame(game: Game) {
|
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const gamePlaytime = gamesPlaytime.get(game.id)!;
|
const gamePlaytime = gamesPlaytime.get(game.id)!;
|
||||||
|
|
||||||
@ -110,7 +48,23 @@ function onTickGame(game: Game) {
|
|||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const onOpenGame = (game: Game) => {
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
gamesPlaytime.set(game.id, {
|
||||||
|
lastTick: now,
|
||||||
|
firstTick: now,
|
||||||
|
lastSyncTick: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (game.remoteId) {
|
||||||
|
updateGamePlaytime(game, 0, new Date()).catch(() => {});
|
||||||
|
} else {
|
||||||
|
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onCloseGame = (game: Game) => {
|
const onCloseGame = (game: Game) => {
|
||||||
const gamePlaytime = gamesPlaytime.get(game.id)!;
|
const gamePlaytime = gamesPlaytime.get(game.id)!;
|
||||||
@ -126,3 +80,50 @@ const onCloseGame = (game: Game) => {
|
|||||||
createGame(game).catch(() => {});
|
createGame(game).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const watchProcesses = async () => {
|
||||||
|
const games = await gameRepository.find({
|
||||||
|
where: {
|
||||||
|
executablePath: Not(IsNull()),
|
||||||
|
isDeleted: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (games.length === 0) return;
|
||||||
|
// const processes = await PythonInstance.getProcessList();
|
||||||
|
const processes = [];
|
||||||
|
|
||||||
|
const processSet = new Set(processes.map((process) => process.exe));
|
||||||
|
|
||||||
|
for (const game of games) {
|
||||||
|
const executablePath = game.executablePath!;
|
||||||
|
|
||||||
|
const gameProcess = processSet.has(executablePath);
|
||||||
|
|
||||||
|
if (gameProcess) {
|
||||||
|
if (gamesPlaytime.has(game.id)) {
|
||||||
|
onGameTick(game);
|
||||||
|
} else {
|
||||||
|
onOpenGame(game);
|
||||||
|
}
|
||||||
|
} else if (gamesPlaytime.has(game.id)) {
|
||||||
|
onCloseGame(game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTick++;
|
||||||
|
|
||||||
|
if (WindowManager.mainWindow) {
|
||||||
|
const gamesRunning = Array.from(gamesPlaytime.entries()).map((entry) => {
|
||||||
|
return {
|
||||||
|
id: entry[0],
|
||||||
|
sessionDurationInMillis: performance.now() - entry[1].firstTick,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
WindowManager.mainWindow.webContents.send(
|
||||||
|
"on-games-running",
|
||||||
|
gamesRunning as Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
import axios, { AxiosInstance } from "axios";
|
|
||||||
import parseTorrent from "parse-torrent";
|
|
||||||
import type {
|
|
||||||
RealDebridAddMagnet,
|
|
||||||
RealDebridTorrentInfo,
|
|
||||||
RealDebridUnrestrictLink,
|
|
||||||
RealDebridUser,
|
|
||||||
} from "@types";
|
|
||||||
|
|
||||||
export class RealDebridClient {
|
|
||||||
private static instance: AxiosInstance;
|
|
||||||
private static baseURL = "https://api.real-debrid.com/rest/1.0";
|
|
||||||
|
|
||||||
static authorize(apiToken: string) {
|
|
||||||
this.instance = axios.create({
|
|
||||||
baseURL: this.baseURL,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${apiToken}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async addMagnet(magnet: string) {
|
|
||||||
const searchParams = new URLSearchParams({ magnet });
|
|
||||||
|
|
||||||
const response = await this.instance.post<RealDebridAddMagnet>(
|
|
||||||
"/torrents/addMagnet",
|
|
||||||
searchParams.toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getTorrentInfo(id: string) {
|
|
||||||
const response = await this.instance.get<RealDebridTorrentInfo>(
|
|
||||||
`/torrents/info/${id}`
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getUser() {
|
|
||||||
const response = await this.instance.get<RealDebridUser>(`/user`);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async selectAllFiles(id: string) {
|
|
||||||
const searchParams = new URLSearchParams({ files: "all" });
|
|
||||||
|
|
||||||
return this.instance.post(
|
|
||||||
`/torrents/selectFiles/${id}`,
|
|
||||||
searchParams.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async unrestrictLink(link: string) {
|
|
||||||
const searchParams = new URLSearchParams({ link });
|
|
||||||
|
|
||||||
const response = await this.instance.post<RealDebridUnrestrictLink>(
|
|
||||||
"/unrestrict/link",
|
|
||||||
searchParams.toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async getAllTorrentsFromUser() {
|
|
||||||
const response =
|
|
||||||
await this.instance.get<RealDebridTorrentInfo[]>("/torrents");
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getTorrentId(magnetUri: string) {
|
|
||||||
const userTorrents = await RealDebridClient.getAllTorrentsFromUser();
|
|
||||||
|
|
||||||
const { infoHash } = await parseTorrent(magnetUri);
|
|
||||||
const userTorrent = userTorrents.find(
|
|
||||||
(userTorrent) => userTorrent.hash === infoHash
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userTorrent) return userTorrent.id;
|
|
||||||
|
|
||||||
const torrent = await RealDebridClient.addMagnet(magnetUri);
|
|
||||||
return torrent.id;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
import type { GameShop } from "@types";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export interface SteamGridResponse {
|
|
||||||
success: boolean;
|
|
||||||
data: {
|
|
||||||
id: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SteamGridGameResponse {
|
|
||||||
data: {
|
|
||||||
platforms: {
|
|
||||||
steam: {
|
|
||||||
metadata: {
|
|
||||||
clienticon: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getSteamGridData = async (
|
|
||||||
objectId: string,
|
|
||||||
path: string,
|
|
||||||
shop: GameShop,
|
|
||||||
params: Record<string, string> = {}
|
|
||||||
): Promise<SteamGridResponse> => {
|
|
||||||
const searchParams = new URLSearchParams(params);
|
|
||||||
|
|
||||||
if (!import.meta.env.MAIN_VITE_STEAMGRIDDB_API_KEY) {
|
|
||||||
throw new Error("MAIN_VITE_STEAMGRIDDB_API_KEY is not set");
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.get(
|
|
||||||
`https://www.steamgriddb.com/api/v2/${path}/${shop}/${objectId}?${searchParams.toString()}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${import.meta.env.MAIN_VITE_STEAMGRIDDB_API_KEY}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSteamGridGameById = async (
|
|
||||||
id: number
|
|
||||||
): Promise<SteamGridGameResponse> => {
|
|
||||||
const response = await axios.get(
|
|
||||||
`https://www.steamgriddb.com/api/public/game/${id}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Referer: "https://www.steamgriddb.com/",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSteamGameClientIcon = async (objectId: string) => {
|
|
||||||
const {
|
|
||||||
data: { id: steamGridGameId },
|
|
||||||
} = await getSteamGridData(objectId, "games", "steam");
|
|
||||||
|
|
||||||
const steamGridGame = await getSteamGridGameById(steamGridGameId);
|
|
||||||
return steamGridGame.data.platforms.steam.metadata.clienticon;
|
|
||||||
};
|
|
1
src/main/vite-env.d.ts
vendored
1
src/main/vite-env.d.ts
vendored
@ -1,7 +1,6 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string;
|
|
||||||
readonly MAIN_VITE_API_URL: string;
|
readonly MAIN_VITE_API_URL: string;
|
||||||
readonly MAIN_VITE_ANALYTICS_API_URL: string;
|
readonly MAIN_VITE_ANALYTICS_API_URL: string;
|
||||||
readonly MAIN_VITE_AUTH_URL: string;
|
readonly MAIN_VITE_AUTH_URL: string;
|
||||||
|
@ -6,45 +6,8 @@
|
|||||||
<title>Hydra</title>
|
<title>Hydra</title>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src * 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://do.featurebase.app/js/sdk.css; img-src 'self' data: local: *; media-src 'self' local: data: *; connect-src *; font-src *;"
|
content="default-src 'self'; script-src *; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *; connect-src *; font-src *;"
|
||||||
/>
|
/>
|
||||||
<script>
|
|
||||||
!(function (e, t) {
|
|
||||||
const a = "featurebase-sdk";
|
|
||||||
function n() {
|
|
||||||
if (!t.getElementById(a)) {
|
|
||||||
var e = t.createElement("script");
|
|
||||||
(e.id = a),
|
|
||||||
(e.src = "https://do.featurebase.app/js/sdk.js"),
|
|
||||||
t
|
|
||||||
.getElementsByTagName("script")[0]
|
|
||||||
.parentNode.insertBefore(
|
|
||||||
e,
|
|
||||||
t.getElementsByTagName("script")[0]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"function" != typeof e.Featurebase &&
|
|
||||||
(e.Featurebase = function () {
|
|
||||||
(e.Featurebase.q = e.Featurebase.q || []).push(arguments);
|
|
||||||
}),
|
|
||||||
"complete" === t.readyState || "interactive" === t.readyState
|
|
||||||
? n()
|
|
||||||
: t.addEventListener("DOMContentLoaded", n);
|
|
||||||
})(window, document);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
Featurebase("initialize_feedback_widget", {
|
|
||||||
organization: "https://hydralauncher.featurebase.app", // Replace this with your organization name, copy-paste the subdomain part from your Featurebase workspace url (e.g. https://*yourorg*.featurebase.app)
|
|
||||||
theme: "light", // required
|
|
||||||
placement: "right", // optional - remove to hide the floating button
|
|
||||||
email: "youruser@example.com", // optional
|
|
||||||
defaultBoard: "yourboardname", // optional - preselect a board
|
|
||||||
locale: "en", // Change the language, view all available languages from https://help.featurebase.app/en/articles/8879098-using-featurebase-in-my-language
|
|
||||||
metadata: null, // Attach session-specific metadata to feedback. Refer to the advanced section for the details: https://help.featurebase.app/en/articles/3774671-advanced#7k8iriyap66
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
@ -127,7 +127,7 @@ export default function Downloads() {
|
|||||||
<DownloadGroup
|
<DownloadGroup
|
||||||
key={group.title}
|
key={group.title}
|
||||||
title={group.title}
|
title={group.title}
|
||||||
library={group.library}
|
library={orderBy(group.library, ["updatedAt"], ["desc"])}
|
||||||
openDeleteGameModal={handleOpenDeleteGameModal}
|
openDeleteGameModal={handleOpenDeleteGameModal}
|
||||||
openGameInstaller={handleOpenGameInstaller}
|
openGameInstaller={handleOpenGameInstaller}
|
||||||
seedingStatus={seedingStatus}
|
seedingStatus={seedingStatus}
|
||||||
|
@ -386,3 +386,4 @@ export * from "./steam.types";
|
|||||||
export * from "./real-debrid.types";
|
export * from "./real-debrid.types";
|
||||||
export * from "./ludusavi.types";
|
export * from "./ludusavi.types";
|
||||||
export * from "./how-long-to-beat.types";
|
export * from "./how-long-to-beat.types";
|
||||||
|
export * from "./torbox.types";
|
||||||
|
@ -1,139 +0,0 @@
|
|||||||
import sys, json, urllib.parse, psutil
|
|
||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
||||||
from torrent_downloader import TorrentDownloader
|
|
||||||
from profile_image_processor import ProfileImageProcessor
|
|
||||||
|
|
||||||
torrent_port = sys.argv[1]
|
|
||||||
http_port = sys.argv[2]
|
|
||||||
rpc_password = sys.argv[3]
|
|
||||||
start_download_payload = sys.argv[4]
|
|
||||||
|
|
||||||
torrent_downloader = None
|
|
||||||
|
|
||||||
if start_download_payload:
|
|
||||||
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
|
|
||||||
torrent_downloader = TorrentDownloader(torrent_port)
|
|
||||||
torrent_downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path'])
|
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
|
||||||
rpc_password_header = 'x-hydra-rpc-password'
|
|
||||||
|
|
||||||
skip_log_routes = [
|
|
||||||
"process-list",
|
|
||||||
"status"
|
|
||||||
]
|
|
||||||
|
|
||||||
def log_error(self, format, *args):
|
|
||||||
sys.stderr.write("%s - - [%s] %s\n" %
|
|
||||||
(self.address_string(),
|
|
||||||
self.log_date_time_string(),
|
|
||||||
format%args))
|
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
|
||||||
for route in self.skip_log_routes:
|
|
||||||
if route in args[0]: return
|
|
||||||
|
|
||||||
super().log_message(format, *args)
|
|
||||||
|
|
||||||
def do_GET(self):
|
|
||||||
if self.path == "/status":
|
|
||||||
if self.headers.get(self.rpc_password_header) != rpc_password:
|
|
||||||
self.send_response(401)
|
|
||||||
self.end_headers()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-type", "application/json")
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
status = torrent_downloader.get_download_status()
|
|
||||||
|
|
||||||
self.wfile.write(json.dumps(status).encode('utf-8'))
|
|
||||||
|
|
||||||
elif self.path == "/seed-status":
|
|
||||||
if self.headers.get(self.rpc_password_header) != rpc_password:
|
|
||||||
self.send_response(401)
|
|
||||||
self.end_headers()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-type", "application/json")
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
status = torrent_downloader.get_seed_status()
|
|
||||||
|
|
||||||
self.wfile.write(json.dumps(status).encode('utf-8'))
|
|
||||||
|
|
||||||
elif self.path == "/healthcheck":
|
|
||||||
self.send_response(200)
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
elif self.path == "/process-list":
|
|
||||||
if self.headers.get(self.rpc_password_header) != rpc_password:
|
|
||||||
self.send_response(401)
|
|
||||||
self.end_headers()
|
|
||||||
return
|
|
||||||
|
|
||||||
process_list = [proc.info for proc in psutil.process_iter(['exe', 'pid', 'username'])]
|
|
||||||
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-type", "application/json")
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
self.wfile.write(json.dumps(process_list).encode('utf-8'))
|
|
||||||
|
|
||||||
def do_POST(self):
|
|
||||||
global torrent_downloader
|
|
||||||
|
|
||||||
if self.headers.get(self.rpc_password_header) != rpc_password:
|
|
||||||
self.send_response(401)
|
|
||||||
self.end_headers()
|
|
||||||
return
|
|
||||||
|
|
||||||
content_length = int(self.headers['Content-Length'])
|
|
||||||
post_data = self.rfile.read(content_length)
|
|
||||||
data = json.loads(post_data.decode('utf-8'))
|
|
||||||
|
|
||||||
if self.path == "/profile-image":
|
|
||||||
parsed_image_path = data['image_path']
|
|
||||||
|
|
||||||
try:
|
|
||||||
parsed_image_path, mime_type = ProfileImageProcessor.process_image(parsed_image_path)
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-type", "application/json")
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
self.wfile.write(json.dumps({'imagePath': parsed_image_path, 'mimeType': mime_type}).encode('utf-8'))
|
|
||||||
except:
|
|
||||||
self.send_response(400)
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
elif self.path == "/action":
|
|
||||||
if torrent_downloader is None:
|
|
||||||
torrent_downloader = TorrentDownloader(torrent_port)
|
|
||||||
|
|
||||||
if data['action'] == 'start':
|
|
||||||
torrent_downloader.start_download(data['game_id'], data['magnet'], data['save_path'])
|
|
||||||
elif data['action'] == 'pause':
|
|
||||||
torrent_downloader.pause_download(data['game_id'])
|
|
||||||
elif data['action'] == 'cancel':
|
|
||||||
torrent_downloader.cancel_download(data['game_id'])
|
|
||||||
elif data['action'] == 'kill-torrent':
|
|
||||||
torrent_downloader.abort_session()
|
|
||||||
torrent_downloader = None
|
|
||||||
elif data['action'] == 'pause-seeding':
|
|
||||||
torrent_downloader.pause_seeding(data['game_id'])
|
|
||||||
elif data['action'] == 'resume-seeding':
|
|
||||||
torrent_downloader.resume_seeding(data['game_id'], data['magnet'], data['save_path'])
|
|
||||||
|
|
||||||
self.send_response(200)
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.send_response(404)
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
httpd = HTTPServer(("", int(http_port)), Handler)
|
|
||||||
httpd.serve_forever()
|
|
@ -1,30 +0,0 @@
|
|||||||
from PIL import Image
|
|
||||||
import os, uuid, tempfile
|
|
||||||
|
|
||||||
class ProfileImageProcessor:
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_parsed_image_data(image_path):
|
|
||||||
Image.MAX_IMAGE_PIXELS = 933120000
|
|
||||||
|
|
||||||
image = Image.open(image_path)
|
|
||||||
|
|
||||||
try:
|
|
||||||
image.seek(1)
|
|
||||||
except EOFError:
|
|
||||||
mime_type = image.get_format_mimetype()
|
|
||||||
return image_path, mime_type
|
|
||||||
else:
|
|
||||||
newUUID = str(uuid.uuid4())
|
|
||||||
new_image_path = os.path.join(tempfile.gettempdir(), newUUID) + ".webp"
|
|
||||||
image.save(new_image_path)
|
|
||||||
|
|
||||||
new_image = Image.open(new_image_path)
|
|
||||||
mime_type = new_image.get_format_mimetype()
|
|
||||||
|
|
||||||
return new_image_path, mime_type
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def process_image(image_path):
|
|
||||||
return ProfileImageProcessor.get_parsed_image_data(image_path)
|
|
@ -1,20 +0,0 @@
|
|||||||
from cx_Freeze import setup, Executable
|
|
||||||
|
|
||||||
# Dependencies are automatically detected, but it might need fine tuning.
|
|
||||||
build_exe_options = {
|
|
||||||
"packages": ["libtorrent"],
|
|
||||||
"build_exe": "hydra-download-manager",
|
|
||||||
"include_msvcr": True
|
|
||||||
}
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="hydra-download-manager",
|
|
||||||
version="0.1",
|
|
||||||
description="Hydra",
|
|
||||||
options={"build_exe": build_exe_options},
|
|
||||||
executables=[Executable(
|
|
||||||
"torrent-client/main.py",
|
|
||||||
target_name="hydra-download-manager",
|
|
||||||
icon="build/icon.ico"
|
|
||||||
)]
|
|
||||||
)
|
|
@ -1,206 +0,0 @@
|
|||||||
import libtorrent as lt
|
|
||||||
|
|
||||||
class TorrentDownloader:
|
|
||||||
def __init__(self, port: str):
|
|
||||||
self.torrent_handles = {}
|
|
||||||
self.downloading_game_id = -1
|
|
||||||
self.session = lt.session({'listen_interfaces': '0.0.0.0:{port}'.format(port=port)})
|
|
||||||
self.trackers = [
|
|
||||||
"udp://tracker.opentrackr.org:1337/announce",
|
|
||||||
"http://tracker.opentrackr.org:1337/announce",
|
|
||||||
"udp://open.tracker.cl:1337/announce",
|
|
||||||
"udp://open.demonii.com:1337/announce",
|
|
||||||
"udp://open.stealth.si:80/announce",
|
|
||||||
"udp://tracker.torrent.eu.org:451/announce",
|
|
||||||
"udp://exodus.desync.com:6969/announce",
|
|
||||||
"udp://tracker.theoks.net:6969/announce",
|
|
||||||
"udp://tracker-udp.gbitt.info:80/announce",
|
|
||||||
"udp://explodie.org:6969/announce",
|
|
||||||
"https://tracker.tamersunion.org:443/announce",
|
|
||||||
"udp://tracker2.dler.org:80/announce",
|
|
||||||
"udp://tracker1.myporn.club:9337/announce",
|
|
||||||
"udp://tracker.tiny-vps.com:6969/announce",
|
|
||||||
"udp://tracker.dler.org:6969/announce",
|
|
||||||
"udp://tracker.bittor.pw:1337/announce",
|
|
||||||
"udp://tracker.0x7c0.com:6969/announce",
|
|
||||||
"udp://retracker01-msk-virt.corbina.net:80/announce",
|
|
||||||
"udp://opentracker.io:6969/announce",
|
|
||||||
"udp://open.free-tracker.ga:6969/announce",
|
|
||||||
"udp://new-line.net:6969/announce",
|
|
||||||
"udp://moonburrow.club:6969/announce",
|
|
||||||
"udp://leet-tracker.moe:1337/announce",
|
|
||||||
"udp://bt2.archive.org:6969/announce",
|
|
||||||
"udp://bt1.archive.org:6969/announce",
|
|
||||||
"http://tracker2.dler.org:80/announce",
|
|
||||||
"http://tracker1.bt.moack.co.kr:80/announce",
|
|
||||||
"http://tracker.dler.org:6969/announce",
|
|
||||||
"http://tr.kxmp.cf:80/announce",
|
|
||||||
"udp://u.peer-exchange.download:6969/announce",
|
|
||||||
"udp://ttk2.nbaonlineservice.com:6969/announce",
|
|
||||||
"udp://tracker.tryhackx.org:6969/announce",
|
|
||||||
"udp://tracker.srv00.com:6969/announce",
|
|
||||||
"udp://tracker.skynetcloud.site:6969/announce",
|
|
||||||
"udp://tracker.jamesthebard.net:6969/announce",
|
|
||||||
"udp://tracker.fnix.net:6969/announce",
|
|
||||||
"udp://tracker.filemail.com:6969/announce",
|
|
||||||
"udp://tracker.farted.net:6969/announce",
|
|
||||||
"udp://tracker.edkj.club:6969/announce",
|
|
||||||
"udp://tracker.dump.cl:6969/announce",
|
|
||||||
"udp://tracker.deadorbit.nl:6969/announce",
|
|
||||||
"udp://tracker.darkness.services:6969/announce",
|
|
||||||
"udp://tracker.ccp.ovh:6969/announce",
|
|
||||||
"udp://tamas3.ynh.fr:6969/announce",
|
|
||||||
"udp://ryjer.com:6969/announce",
|
|
||||||
"udp://run.publictracker.xyz:6969/announce",
|
|
||||||
"udp://public.tracker.vraphim.com:6969/announce",
|
|
||||||
"udp://p4p.arenabg.com:1337/announce",
|
|
||||||
"udp://p2p.publictracker.xyz:6969/announce",
|
|
||||||
"udp://open.u-p.pw:6969/announce",
|
|
||||||
"udp://open.publictracker.xyz:6969/announce",
|
|
||||||
"udp://open.dstud.io:6969/announce",
|
|
||||||
"udp://open.demonoid.ch:6969/announce",
|
|
||||||
"udp://odd-hd.fr:6969/announce",
|
|
||||||
"udp://martin-gebhardt.eu:25/announce",
|
|
||||||
"udp://jutone.com:6969/announce",
|
|
||||||
"udp://isk.richardsw.club:6969/announce",
|
|
||||||
"udp://evan.im:6969/announce",
|
|
||||||
"udp://epider.me:6969/announce",
|
|
||||||
"udp://d40969.acod.regrucolo.ru:6969/announce",
|
|
||||||
"udp://bt.rer.lol:6969/announce",
|
|
||||||
"udp://amigacity.xyz:6969/announce",
|
|
||||||
"udp://1c.premierzal.ru:6969/announce",
|
|
||||||
"https://trackers.run:443/announce",
|
|
||||||
"https://tracker.yemekyedim.com:443/announce",
|
|
||||||
"https://tracker.renfei.net:443/announce",
|
|
||||||
"https://tracker.pmman.tech:443/announce",
|
|
||||||
"https://tracker.lilithraws.org:443/announce",
|
|
||||||
"https://tracker.imgoingto.icu:443/announce",
|
|
||||||
"https://tracker.cloudit.top:443/announce",
|
|
||||||
"https://tracker-zhuqiy.dgj055.icu:443/announce",
|
|
||||||
"http://tracker.renfei.net:8080/announce",
|
|
||||||
"http://tracker.mywaifu.best:6969/announce",
|
|
||||||
"http://tracker.ipv6tracker.org:80/announce",
|
|
||||||
"http://tracker.files.fm:6969/announce",
|
|
||||||
"http://tracker.edkj.club:6969/announce",
|
|
||||||
"http://tracker.bt4g.com:2095/announce",
|
|
||||||
"http://tracker-zhuqiy.dgj055.icu:80/announce",
|
|
||||||
"http://t1.aag.moe:17715/announce",
|
|
||||||
"http://t.overflow.biz:6969/announce",
|
|
||||||
"http://bittorrent-tracker.e-n-c-r-y-p-t.net:1337/announce",
|
|
||||||
"udp://torrents.artixlinux.org:6969/announce",
|
|
||||||
"udp://mail.artixlinux.org:6969/announce",
|
|
||||||
"udp://ipv4.rer.lol:2710/announce",
|
|
||||||
"udp://concen.org:6969/announce",
|
|
||||||
"udp://bt.rer.lol:2710/announce",
|
|
||||||
"udp://aegir.sexy:6969/announce",
|
|
||||||
"https://www.peckservers.com:9443/announce",
|
|
||||||
"https://tracker.ipfsscan.io:443/announce",
|
|
||||||
"https://tracker.gcrenwp.top:443/announce",
|
|
||||||
"http://www.peckservers.com:9000/announce",
|
|
||||||
"http://tracker1.itzmx.com:8080/announce",
|
|
||||||
"http://ch3oh.ru:6969/announce",
|
|
||||||
"http://bvarf.tracker.sh:2086/announce",
|
|
||||||
]
|
|
||||||
|
|
||||||
def start_download(self, game_id: int, magnet: str, save_path: str):
|
|
||||||
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers}
|
|
||||||
torrent_handle = self.session.add_torrent(params)
|
|
||||||
self.torrent_handles[game_id] = torrent_handle
|
|
||||||
torrent_handle.set_flags(lt.torrent_flags.auto_managed)
|
|
||||||
torrent_handle.resume()
|
|
||||||
|
|
||||||
self.downloading_game_id = game_id
|
|
||||||
|
|
||||||
def pause_download(self, game_id: int):
|
|
||||||
torrent_handle = self.torrent_handles.get(game_id)
|
|
||||||
if torrent_handle:
|
|
||||||
torrent_handle.pause()
|
|
||||||
torrent_handle.unset_flags(lt.torrent_flags.auto_managed)
|
|
||||||
self.downloading_game_id = -1
|
|
||||||
|
|
||||||
def cancel_download(self, game_id: int):
|
|
||||||
torrent_handle = self.torrent_handles.get(game_id)
|
|
||||||
if torrent_handle:
|
|
||||||
torrent_handle.pause()
|
|
||||||
self.session.remove_torrent(torrent_handle)
|
|
||||||
self.torrent_handles[game_id] = None
|
|
||||||
self.downloading_game_id = -1
|
|
||||||
|
|
||||||
def abort_session(self):
|
|
||||||
for game_id in self.torrent_handles:
|
|
||||||
torrent_handle = self.torrent_handles[game_id]
|
|
||||||
torrent_handle.pause()
|
|
||||||
self.session.remove_torrent(torrent_handle)
|
|
||||||
|
|
||||||
self.session.abort()
|
|
||||||
self.torrent_handles = {}
|
|
||||||
self.downloading_game_id = -1
|
|
||||||
|
|
||||||
def get_download_status(self):
|
|
||||||
if self.downloading_game_id == -1:
|
|
||||||
return None
|
|
||||||
|
|
||||||
torrent_handle = self.torrent_handles.get(self.downloading_game_id)
|
|
||||||
|
|
||||||
status = torrent_handle.status()
|
|
||||||
info = torrent_handle.get_torrent_info()
|
|
||||||
|
|
||||||
response = {
|
|
||||||
'folderName': info.name() if info else "",
|
|
||||||
'fileSize': info.total_size() if info else 0,
|
|
||||||
'gameId': self.downloading_game_id,
|
|
||||||
'progress': status.progress,
|
|
||||||
'downloadSpeed': status.download_rate,
|
|
||||||
'numPeers': status.num_peers,
|
|
||||||
'numSeeds': status.num_seeds,
|
|
||||||
'status': status.state,
|
|
||||||
'bytesDownloaded': status.progress * info.total_size() if info else status.all_time_download,
|
|
||||||
}
|
|
||||||
|
|
||||||
if status.progress == 1:
|
|
||||||
self.downloading_game_id = -1
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def get_seed_status(self):
|
|
||||||
response = []
|
|
||||||
|
|
||||||
for game_id, torrent_handle in self.torrent_handles.items():
|
|
||||||
if game_id == self.downloading_game_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
status = torrent_handle.status()
|
|
||||||
info = torrent_handle.torrent_file()
|
|
||||||
|
|
||||||
torrent_info = {
|
|
||||||
'folderName': info.name() if info else "",
|
|
||||||
'fileSize': info.total_size() if info else 0,
|
|
||||||
'gameId': game_id,
|
|
||||||
'progress': status.progress,
|
|
||||||
'downloadSpeed': status.download_rate,
|
|
||||||
'uploadSpeed': status.upload_rate,
|
|
||||||
'numPeers': status.num_peers,
|
|
||||||
'numSeeds': status.num_seeds,
|
|
||||||
'status': status.state,
|
|
||||||
'bytesDownloaded': status.progress * info.total_size() if info else status.all_time_download,
|
|
||||||
}
|
|
||||||
|
|
||||||
if status.state == 5:
|
|
||||||
response.append(torrent_info)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def pause_seeding(self, game_id: int):
|
|
||||||
torrent_handle = self.torrent_handles.get(game_id)
|
|
||||||
if torrent_handle:
|
|
||||||
torrent_handle.pause()
|
|
||||||
torrent_handle.unset_flags(lt.torrent_flags.auto_managed)
|
|
||||||
self.session.remove_torrent(torrent_handle)
|
|
||||||
self.torrent_handles.pop(game_id, None)
|
|
||||||
|
|
||||||
def resume_seeding(self, game_id: int, magnet: str, save_path: str):
|
|
||||||
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers}
|
|
||||||
torrent_handle = self.session.add_torrent(params)
|
|
||||||
self.torrent_handles[game_id] = torrent_handle
|
|
||||||
torrent_handle.set_flags(lt.torrent_flags.auto_managed)
|
|
||||||
torrent_handle.resume()
|
|
Loading…
Reference in New Issue
Block a user