mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-22 02:13:59 +00:00
Update PR #1452 to latest HydraLauncher and fix conflicts
This commit is contained in:
@@ -14,9 +14,6 @@ import "./catalogue/get-developers";
|
||||
import "./hardware/get-disk-free-space";
|
||||
import "./hardware/check-folder-write-permission";
|
||||
import "./library/add-game-to-library";
|
||||
import "./library/add-custom-game-to-library";
|
||||
import "./library/update-custom-game";
|
||||
import "./library/update-game-custom-assets";
|
||||
import "./library/add-game-to-favorites";
|
||||
import "./library/remove-game-from-favorites";
|
||||
import "./library/toggle-game-pin";
|
||||
@@ -40,9 +37,7 @@ import "./library/reset-game-achievements";
|
||||
import "./library/change-game-playtime";
|
||||
import "./library/toggle-automatic-cloud-sync";
|
||||
import "./library/get-default-wine-prefix-selection-path";
|
||||
import "./library/cleanup-unused-assets";
|
||||
import "./library/create-steam-shortcut";
|
||||
import "./library/copy-custom-game-asset";
|
||||
import "./misc/open-checkout";
|
||||
import "./misc/open-external";
|
||||
import "./misc/show-open-dialog";
|
||||
@@ -51,10 +46,9 @@ import "./misc/show-item-in-folder";
|
||||
import "./misc/get-badges";
|
||||
import "./misc/install-common-redist";
|
||||
import "./misc/can-install-common-redist";
|
||||
import "./misc/save-temp-file";
|
||||
import "./misc/delete-temp-file";
|
||||
import "./torrenting/cancel-game-download";
|
||||
import "./torrenting/pause-game-download";
|
||||
import "./user-preferences/authenticate-all-debrid";
|
||||
import "./torrenting/resume-game-download";
|
||||
import "./torrenting/start-game-download";
|
||||
import "./torrenting/pause-game-seed";
|
||||
|
||||
17
src/main/events/user-preferences/authenticate-all-debrid.ts
Normal file
17
src/main/events/user-preferences/authenticate-all-debrid.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { AllDebridClient } from "@main/services/download/all-debrid";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const authenticateAllDebrid = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
apiKey: string
|
||||
) => {
|
||||
AllDebridClient.authorize(apiKey);
|
||||
const result = await AllDebridClient.getUser();
|
||||
if ("error_code" in result) {
|
||||
return { error_code: result.error_code };
|
||||
}
|
||||
|
||||
return result.user;
|
||||
};
|
||||
|
||||
registerEvent("authenticateAllDebrid", authenticateAllDebrid);
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CommonRedistManager,
|
||||
TorBoxClient,
|
||||
RealDebridClient,
|
||||
AllDebridClient,
|
||||
Aria2,
|
||||
DownloadManager,
|
||||
HydraApi,
|
||||
@@ -37,6 +38,10 @@ export const loadState = async () => {
|
||||
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||
}
|
||||
|
||||
if (userPreferences?.allDebridApiKey) {
|
||||
AllDebridClient.authorize(userPreferences.allDebridApiKey);
|
||||
}
|
||||
|
||||
if (userPreferences?.torBoxApiToken) {
|
||||
TorBoxClient.authorize(userPreferences.torBoxApiToken);
|
||||
}
|
||||
|
||||
315
src/main/services/download/all-debrid.ts
Normal file
315
src/main/services/download/all-debrid.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import type { AllDebridUser } from "@types";
|
||||
import { logger } from "@main/services";
|
||||
|
||||
interface AllDebridMagnetStatus {
|
||||
id: number;
|
||||
filename: string;
|
||||
size: number;
|
||||
status: string;
|
||||
statusCode: number;
|
||||
downloaded: number;
|
||||
uploaded: number;
|
||||
seeders: number;
|
||||
downloadSpeed: number;
|
||||
uploadSpeed: number;
|
||||
uploadDate: number;
|
||||
completionDate: number;
|
||||
links: Array<{
|
||||
link: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface AllDebridError {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface AllDebridDownloadUrl {
|
||||
link: string;
|
||||
size?: number;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export class AllDebridClient {
|
||||
private static instance: AxiosInstance;
|
||||
private static readonly baseURL = "https://api.alldebrid.com/v4";
|
||||
|
||||
static authorize(apiKey: string) {
|
||||
logger.info("[AllDebrid] Authorizing with key:", apiKey ? "***" : "empty");
|
||||
this.instance = axios.create({
|
||||
baseURL: this.baseURL,
|
||||
params: {
|
||||
agent: "hydra",
|
||||
apikey: apiKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async getUser() {
|
||||
try {
|
||||
const response = await this.instance.get<{
|
||||
status: string;
|
||||
data?: { user: AllDebridUser };
|
||||
error?: AllDebridError;
|
||||
}>("/user");
|
||||
|
||||
logger.info("[AllDebrid] API Response:", response.data);
|
||||
|
||||
if (response.data.status === "error") {
|
||||
const error = response.data.error;
|
||||
logger.error("[AllDebrid] API Error:", error);
|
||||
if (error?.code === "AUTH_MISSING_APIKEY") {
|
||||
return { error_code: "alldebrid_missing_key" };
|
||||
}
|
||||
if (error?.code === "AUTH_BAD_APIKEY") {
|
||||
return { error_code: "alldebrid_invalid_key" };
|
||||
}
|
||||
if (error?.code === "AUTH_BLOCKED") {
|
||||
return { error_code: "alldebrid_blocked" };
|
||||
}
|
||||
if (error?.code === "AUTH_USER_BANNED") {
|
||||
return { error_code: "alldebrid_banned" };
|
||||
}
|
||||
return { error_code: "alldebrid_unknown_error" };
|
||||
}
|
||||
|
||||
if (!response.data.data?.user) {
|
||||
logger.error("[AllDebrid] No user data in response");
|
||||
return { error_code: "alldebrid_invalid_response" };
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"[AllDebrid] Successfully got user:",
|
||||
response.data.data.user.username
|
||||
);
|
||||
return { user: response.data.data.user };
|
||||
} catch (error: any) {
|
||||
logger.error("[AllDebrid] Request Error:", error);
|
||||
if (error.response?.data?.error) {
|
||||
return { error_code: "alldebrid_invalid_key" };
|
||||
}
|
||||
return { error_code: "alldebrid_network_error" };
|
||||
}
|
||||
}
|
||||
|
||||
private static async uploadMagnet(magnet: string) {
|
||||
try {
|
||||
logger.info("[AllDebrid] Uploading magnet with params:", { magnet });
|
||||
|
||||
const response = await this.instance.get("/magnet/upload", {
|
||||
params: {
|
||||
magnets: [magnet],
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
"[AllDebrid] Upload Magnet Raw Response:",
|
||||
JSON.stringify(response.data, null, 2)
|
||||
);
|
||||
|
||||
if (response.data.status === "error") {
|
||||
throw new Error(response.data.error?.message || "Unknown error");
|
||||
}
|
||||
|
||||
const magnetInfo = response.data.data.magnets[0];
|
||||
logger.info(
|
||||
"[AllDebrid] Magnet Info:",
|
||||
JSON.stringify(magnetInfo, null, 2)
|
||||
);
|
||||
|
||||
if (magnetInfo.error) {
|
||||
throw new Error(magnetInfo.error.message);
|
||||
}
|
||||
|
||||
return magnetInfo.id;
|
||||
} catch (error: any) {
|
||||
logger.error("[AllDebrid] Upload Magnet Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static async checkMagnetStatus(
|
||||
magnetId: number
|
||||
): Promise<AllDebridMagnetStatus> {
|
||||
try {
|
||||
logger.info("[AllDebrid] Checking magnet status for ID:", magnetId);
|
||||
|
||||
const response = await this.instance.get(`/magnet/status`, {
|
||||
params: {
|
||||
id: magnetId,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
"[AllDebrid] Check Magnet Status Raw Response:",
|
||||
JSON.stringify(response.data, null, 2)
|
||||
);
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error("No response data received");
|
||||
}
|
||||
|
||||
if (response.data.status === "error") {
|
||||
throw new Error(response.data.error?.message || "Unknown error");
|
||||
}
|
||||
|
||||
// Verificăm noua structură a răspunsului
|
||||
const magnetData = response.data.data?.magnets;
|
||||
if (!magnetData || typeof magnetData !== "object") {
|
||||
logger.error(
|
||||
"[AllDebrid] Invalid response structure:",
|
||||
JSON.stringify(response.data, null, 2)
|
||||
);
|
||||
throw new Error("Invalid magnet status response format");
|
||||
}
|
||||
|
||||
// Convertim răspunsul în formatul așteptat
|
||||
const magnetStatus: AllDebridMagnetStatus = {
|
||||
id: magnetData.id,
|
||||
filename: magnetData.filename,
|
||||
size: magnetData.size,
|
||||
status: magnetData.status,
|
||||
statusCode: magnetData.statusCode,
|
||||
downloaded: magnetData.downloaded,
|
||||
uploaded: magnetData.uploaded,
|
||||
seeders: magnetData.seeders,
|
||||
downloadSpeed: magnetData.downloadSpeed,
|
||||
uploadSpeed: magnetData.uploadSpeed,
|
||||
uploadDate: magnetData.uploadDate,
|
||||
completionDate: magnetData.completionDate,
|
||||
links: magnetData.links.map((link) => ({
|
||||
link: link.link,
|
||||
filename: link.filename,
|
||||
size: link.size,
|
||||
})),
|
||||
};
|
||||
|
||||
logger.info(
|
||||
"[AllDebrid] Magnet Status:",
|
||||
JSON.stringify(magnetStatus, null, 2)
|
||||
);
|
||||
|
||||
return magnetStatus;
|
||||
} catch (error: any) {
|
||||
logger.error("[AllDebrid] Check Magnet Status Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static async unlockLink(link: string) {
|
||||
try {
|
||||
const response = await this.instance.get<{
|
||||
status: string;
|
||||
data?: { link: string };
|
||||
error?: AllDebridError;
|
||||
}>("/link/unlock", {
|
||||
params: {
|
||||
link,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data.status === "error") {
|
||||
throw new Error(response.data.error?.message || "Unknown error");
|
||||
}
|
||||
|
||||
const unlockedLink = response.data.data?.link;
|
||||
if (!unlockedLink) {
|
||||
throw new Error("No download link received from AllDebrid");
|
||||
}
|
||||
|
||||
return unlockedLink;
|
||||
} catch (error: any) {
|
||||
logger.error("[AllDebrid] Unlock Link Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getDownloadUrls(
|
||||
uri: string
|
||||
): Promise<AllDebridDownloadUrl[]> {
|
||||
try {
|
||||
logger.info("[AllDebrid] Getting download URLs for URI:", uri);
|
||||
|
||||
if (uri.startsWith("magnet:")) {
|
||||
logger.info("[AllDebrid] Detected magnet link, uploading...");
|
||||
// 1. Upload magnet
|
||||
const magnetId = await this.uploadMagnet(uri);
|
||||
logger.info("[AllDebrid] Magnet uploaded, ID:", magnetId);
|
||||
|
||||
// 2. Verificăm statusul până când avem link-uri
|
||||
let retries = 0;
|
||||
let magnetStatus: AllDebridMagnetStatus;
|
||||
|
||||
do {
|
||||
magnetStatus = await this.checkMagnetStatus(magnetId);
|
||||
logger.info(
|
||||
"[AllDebrid] Magnet status:",
|
||||
magnetStatus.status,
|
||||
"statusCode:",
|
||||
magnetStatus.statusCode
|
||||
);
|
||||
|
||||
if (magnetStatus.statusCode === 4) {
|
||||
// Ready
|
||||
// Deblocăm fiecare link în parte și aruncăm eroare dacă oricare eșuează
|
||||
const unlockedLinks = await Promise.all(
|
||||
magnetStatus.links.map(async (link) => {
|
||||
try {
|
||||
const unlockedLink = await this.unlockLink(link.link);
|
||||
logger.info(
|
||||
"[AllDebrid] Successfully unlocked link:",
|
||||
unlockedLink
|
||||
);
|
||||
|
||||
return {
|
||||
link: unlockedLink,
|
||||
size: link.size,
|
||||
filename: link.filename,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[AllDebrid] Failed to unlock link:",
|
||||
link.link,
|
||||
error
|
||||
);
|
||||
throw new Error("Failed to unlock all links");
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
logger.info(
|
||||
"[AllDebrid] Got unlocked download links:",
|
||||
unlockedLinks
|
||||
);
|
||||
console.log("[AllDebrid] FINAL LINKS →", unlockedLinks);
|
||||
return unlockedLinks;
|
||||
}
|
||||
|
||||
if (retries++ > 30) {
|
||||
// Maximum 30 de încercări
|
||||
throw new Error("Timeout waiting for magnet to be ready");
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // Așteptăm 2 secunde între verificări
|
||||
} while (magnetStatus.statusCode !== 4);
|
||||
} else {
|
||||
logger.info("[AllDebrid] Regular link, unlocking...");
|
||||
// Pentru link-uri normale, doar debridam link-ul
|
||||
const downloadUrl = await this.unlockLink(uri);
|
||||
logger.info("[AllDebrid] Got unlocked download URL:", downloadUrl);
|
||||
return [
|
||||
{
|
||||
link: downloadUrl,
|
||||
},
|
||||
];
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("[AllDebrid] Get Download URLs Error:", error);
|
||||
throw error;
|
||||
}
|
||||
return []; // Add default return for TypeScript
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "./types";
|
||||
import { calculateETA, getDirSize } from "./helpers";
|
||||
import { RealDebridClient } from "./real-debrid";
|
||||
import { AllDebridClient } from "./all-debrid";
|
||||
import path from "path";
|
||||
import { logger } from "../logger";
|
||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
@@ -40,6 +41,7 @@ export class DownloadManager {
|
||||
})
|
||||
: undefined,
|
||||
downloadsToSeed?.map((download) => ({
|
||||
action: "seed",
|
||||
game_id: levelKeys.game(download.shop, download.objectId),
|
||||
url: download.uri,
|
||||
save_path: download.downloadPath,
|
||||
@@ -377,6 +379,27 @@ export class DownloadManager {
|
||||
allow_multiple_connections: true,
|
||||
};
|
||||
}
|
||||
case Downloader.AllDebrid: {
|
||||
const downloadUrls = await AllDebridClient.getDownloadUrls(
|
||||
download.uri
|
||||
);
|
||||
|
||||
if (!downloadUrls.length)
|
||||
throw new Error(DownloadError.NotCachedInAllDebrid);
|
||||
|
||||
const totalSize = downloadUrls.reduce(
|
||||
(total, url) => total + (url.size || 0),
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
action: "start",
|
||||
game_id: downloadId,
|
||||
url: downloadUrls.map((d) => d.link),
|
||||
save_path: download.downloadPath,
|
||||
total_size: totalSize,
|
||||
};
|
||||
}
|
||||
case Downloader.TorBox: {
|
||||
const { name, url } = await TorBoxClient.getDownloadInfo(download.uri);
|
||||
|
||||
|
||||
@@ -17,17 +17,24 @@ export const calculateETA = (
|
||||
};
|
||||
|
||||
export const getDirSize = async (dir: string): Promise<number> => {
|
||||
const getItemSize = async (filePath: string): Promise<number> => {
|
||||
const stat = await fs.promises.stat(filePath);
|
||||
try {
|
||||
const stat = await fs.promises.stat(dir);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
return getDirSize(filePath);
|
||||
// If it's a file, return its size directly
|
||||
if (!stat.isDirectory()) {
|
||||
return stat.size;
|
||||
}
|
||||
|
||||
return stat.size;
|
||||
};
|
||||
const getItemSize = async (filePath: string): Promise<number> => {
|
||||
const stat = await fs.promises.stat(filePath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
return getDirSize(filePath);
|
||||
}
|
||||
|
||||
return stat.size;
|
||||
};
|
||||
|
||||
try {
|
||||
const files = await fs.promises.readdir(dir);
|
||||
const filePaths = files.map((file) => path.join(dir, file));
|
||||
const sizes = await Promise.all(filePaths.map(getItemSize));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./download-manager";
|
||||
export * from "./real-debrid";
|
||||
export * from "./all-debrid";
|
||||
export * from "./torbox";
|
||||
|
||||
@@ -11,9 +11,13 @@ import { app, dialog } from "electron";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
|
||||
interface GamePayload {
|
||||
action: string;
|
||||
game_id: string;
|
||||
url: string;
|
||||
url: string | string[];
|
||||
save_path: string;
|
||||
header?: string;
|
||||
out?: string;
|
||||
total_size?: number;
|
||||
}
|
||||
|
||||
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
||||
|
||||
Reference in New Issue
Block a user