diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 745f531e..b19b90c3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,8 +10,7 @@ jobs:
build:
strategy:
matrix:
- # os: [windows-latest, ubuntu-latest]
- os: [windows-latest]
+ os: [windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index fdd1f103..818d2d9c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -33,6 +33,16 @@ jobs:
with:
python-version: 3.9
+ - name: Install Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ components: rustfmt
+
+ - name: Build Rust
+ run: cargo build --release
+ working-directory: ./rust_rpc
+
- name: Install dependencies
run: pip install -r requirements.txt
diff --git a/binaries/hydra-httpdl.exe b/binaries/hydra-httpdl.exe
new file mode 100644
index 00000000..7a686d9e
Binary files /dev/null and b/binaries/hydra-httpdl.exe differ
diff --git a/package.json b/package.json
index bbfca31d..3eae26f9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
- "version": "3.4.0",
+ "version": "3.4.1",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
diff --git a/python_rpc/http_downloader.py b/python_rpc/http_downloader.py
index 95e735a9..b61688fc 100644
--- a/python_rpc/http_downloader.py
+++ b/python_rpc/http_downloader.py
@@ -8,7 +8,7 @@ class HttpDownloader:
self.process = None
self.last_status = None
- def start_download(self, url: str, save_path: str, header: str = None, out: str = None, allow_multiple_connections: bool = False, connections_limit: int = 1):
+ def start_download(self, url: str, save_path: str, header: str = None, allow_multiple_connections: bool = False, connections_limit: int = 1):
cmd = [self.hydra_exe]
cmd.append(url)
diff --git a/python_rpc/main.py b/python_rpc/main.py
index 4202a871..864b4e50 100644
--- a/python_rpc/main.py
+++ b/python_rpc/main.py
@@ -36,7 +36,7 @@ if start_download_payload:
http_downloader = HttpDownloader(hydra_httpdl_bin)
downloads[initial_download['game_id']] = http_downloader
try:
- http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
+ http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get('allow_multiple_connections', False), initial_download.get('connections_limit', 24))
except Exception as e:
print("Error starting http download", e)
@@ -148,11 +148,11 @@ def action():
torrent_downloader.start_download(url, data['save_path'])
else:
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
- existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'), data.get('allow_multiple_connections', False), data.get('connections_limit', 24))
+ existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('allow_multiple_connections', False), data.get('connections_limit', 24))
else:
http_downloader = HttpDownloader(hydra_httpdl_bin)
downloads[game_id] = http_downloader
- http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'), data.get('allow_multiple_connections', False), data.get('connections_limit', 24))
+ http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('allow_multiple_connections', False), data.get('connections_limit', 24))
downloading_game_id = game_id
diff --git a/rust_rpc/src/main.rs b/rust_rpc/src/main.rs
index 9a8b3d14..884366a8 100644
--- a/rust_rpc/src/main.rs
+++ b/rust_rpc/src/main.rs
@@ -807,13 +807,11 @@ impl ResumeManager {
match HydraHeader::read_from_file(&mut reader) {
Ok(header) => {
let current_url_hash = Sha256::digest(url.as_bytes());
- let current_etag_hash = Sha256::digest(etag.as_bytes());
let url_matches = header.url_hash == current_url_hash.as_slice();
- let etag_matches = header.etag == current_etag_hash.as_slice();
let size_matches = header.file_size == file_size;
- if url_matches && etag_matches && size_matches {
+ if url_matches && size_matches {
return Ok(Self {
header,
file_path: path.to_string(),
diff --git a/scripts/postinstall.cjs b/scripts/postinstall.cjs
index 25d27c0a..b7099027 100644
--- a/scripts/postinstall.cjs
+++ b/scripts/postinstall.cjs
@@ -47,3 +47,10 @@ const downloadLudusavi = async () => {
};
downloadLudusavi();
+
+if (process.platform !== "win32") {
+ const binariesPath = path.join(__dirname, "..", "binaries");
+
+ fs.chmodSync(path.join(binariesPath, "7zz"), 0o755);
+ fs.chmodSync(path.join(binariesPath, "7zzs"), 0o755);
+}
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index bfab174f..8b3893b3 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -499,6 +499,7 @@
"animated_profile_banner": "Animated profile banner",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "You've just discovered a Hydra Cloud feature!",
- "learn_more": "Learn More"
+ "learn_more": "Learn More",
+ "debrid_description": "Download up to 4x faster with Nimbus"
}
}
diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json
index 65a97dc3..41c58ebf 100644
--- a/src/locales/pt-BR/translation.json
+++ b/src/locales/pt-BR/translation.json
@@ -494,6 +494,7 @@
"animated_profile_banner": "Banner animado no perfil",
"cloud_saving": "Saves de jogos em nuvem",
"hydra_cloud_feature_found": "Você descobriu uma funcionalidade Hydra Cloud!",
- "learn_more": "Saiba mais"
+ "learn_more": "Saiba mais",
+ "debrid_description": "Baixe até 4x mais rápido com Nimbus"
}
}
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index 8465843f..a0b2296b 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -47,6 +47,7 @@ import "./torrenting/resume-game-download";
import "./torrenting/start-game-download";
import "./torrenting/pause-game-seed";
import "./torrenting/resume-game-seed";
+import "./torrenting/check-debrid-availability";
import "./user-preferences/get-user-preferences";
import "./user-preferences/update-user-preferences";
import "./user-preferences/auto-launch";
diff --git a/src/main/events/library/delete-game-folder.ts b/src/main/events/library/delete-game-folder.ts
index 9c290fe0..b9cef25b 100644
--- a/src/main/events/library/delete-game-folder.ts
+++ b/src/main/events/library/delete-game-folder.ts
@@ -13,35 +13,42 @@ const deleteGameFolder = async (
objectId: string
): Promise => {
const downloadKey = levelKeys.game(shop, objectId);
-
const download = await downloadsSublevel.get(downloadKey);
- if (!download) return;
+ if (!download?.folderName) return;
- if (download.folderName) {
- const folderPath = path.join(
- download.downloadPath ?? (await getDownloadsPath()),
- download.folderName
- );
+ const folderPath = path.join(
+ download.downloadPath ?? (await getDownloadsPath()),
+ download.folderName
+ );
- if (fs.existsSync(folderPath)) {
+ const metaPath = `${folderPath}.meta`;
+
+ const deleteFile = async (filePath: string, isDirectory = false) => {
+ if (fs.existsSync(filePath)) {
await new Promise((resolve, reject) => {
fs.rm(
- folderPath,
- { recursive: true, force: true, maxRetries: 5, retryDelay: 200 },
+ filePath,
+ {
+ recursive: isDirectory,
+ force: true,
+ maxRetries: 5,
+ retryDelay: 200,
+ },
(error) => {
if (error) {
logger.error(error);
reject();
}
-
resolve();
}
);
});
}
- }
+ };
+ await deleteFile(folderPath, true);
+ await deleteFile(metaPath);
await downloadsSublevel.del(downloadKey);
};
diff --git a/src/main/events/torrenting/check-debrid-availability.ts b/src/main/events/torrenting/check-debrid-availability.ts
new file mode 100644
index 00000000..447c3e45
--- /dev/null
+++ b/src/main/events/torrenting/check-debrid-availability.ts
@@ -0,0 +1,11 @@
+import { HydraDebridClient } from "@main/services/download/hydra-debrid";
+import { registerEvent } from "../register-event";
+
+const checkDebridAvailability = async (
+ _event: Electron.IpcMainInvokeEvent,
+ magnets: string[]
+) => {
+ return HydraDebridClient.getAvailableMagnets(magnets);
+};
+
+registerEvent("checkDebridAvailability", checkDebridAvailability);
diff --git a/src/main/services/7zip.ts b/src/main/services/7zip.ts
index 08abf389..9a9f85be 100644
--- a/src/main/services/7zip.ts
+++ b/src/main/services/7zip.ts
@@ -50,8 +50,6 @@ export class SevenZip {
});
child.once("exit", (code) => {
- console.log("EXIT CALLED", code, filePath);
-
if (code === 0) {
successCb();
return;
diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts
index a079f360..c3a104cb 100644
--- a/src/main/services/download/download-manager.ts
+++ b/src/main/services/download/download-manager.ts
@@ -23,6 +23,7 @@ import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { sortBy } from "lodash-es";
import { TorBoxClient } from "./torbox";
import { GameFilesManager } from "../game-files-manager";
+import { HydraDebridClient } from "./hydra-debrid";
export class DownloadManager {
private static downloadingGameId: string | null = null;
@@ -389,6 +390,21 @@ export class DownloadManager {
allow_multiple_connections: true,
};
}
+ case Downloader.Hydra: {
+ const downloadUrl = await HydraDebridClient.getDownloadUrl(
+ download.uri
+ );
+
+ if (!downloadUrl) throw new Error(DownloadError.NotCachedInHydra);
+
+ return {
+ action: "start",
+ game_id: downloadId,
+ url: downloadUrl,
+ save_path: download.downloadPath,
+ allow_multiple_connections: true,
+ };
+ }
}
}
diff --git a/src/main/services/download/hydra-debrid.ts b/src/main/services/download/hydra-debrid.ts
new file mode 100644
index 00000000..d9d40fef
--- /dev/null
+++ b/src/main/services/download/hydra-debrid.ts
@@ -0,0 +1,27 @@
+import { HydraApi } from "../hydra-api";
+
+export class HydraDebridClient {
+ public static getAvailableMagnets(
+ magnets: string[]
+ ): Promise> {
+ return HydraApi.put(
+ "/debrid/check-availability",
+ {
+ magnets,
+ },
+ { needsAuth: false }
+ );
+ }
+
+ public static async getDownloadUrl(magnet: string) {
+ try {
+ const response = await HydraApi.post("/debrid/request-file", {
+ magnet,
+ });
+
+ return response.downloadUrl;
+ } catch (error) {
+ return null;
+ }
+ }
+}
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 280c0cc4..a7e06f90 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -55,6 +55,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.on("on-seeding-status", listener);
return () => ipcRenderer.removeListener("on-seeding-status", listener);
},
+ checkDebridAvailability: (magnets: string[]) =>
+ ipcRenderer.invoke("checkDebridAvailability", magnets),
/* Catalogue */
searchGames: (payload: CatalogueSearchPayload, take: number, skip: number) =>
diff --git a/src/renderer/src/assets/meteor.svg b/src/renderer/src/assets/meteor.svg
new file mode 100644
index 00000000..95174efa
--- /dev/null
+++ b/src/renderer/src/assets/meteor.svg
@@ -0,0 +1,24 @@
+
diff --git a/src/renderer/src/components/debrid-badge/debrid-badge.scss b/src/renderer/src/components/debrid-badge/debrid-badge.scss
new file mode 100644
index 00000000..16ef5464
--- /dev/null
+++ b/src/renderer/src/components/debrid-badge/debrid-badge.scss
@@ -0,0 +1,11 @@
+.debrid-badge {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ border-radius: 4px;
+ border: 1px solid rgba(12, 241, 202, 0.3);
+ background: rgba(12, 241, 202, 0.05);
+ color: #0cf1ca;
+ padding: 4px 8px;
+ font-size: 12px;
+}
diff --git a/src/renderer/src/components/debrid-badge/debrid-badge.tsx b/src/renderer/src/components/debrid-badge/debrid-badge.tsx
new file mode 100644
index 00000000..12cd4861
--- /dev/null
+++ b/src/renderer/src/components/debrid-badge/debrid-badge.tsx
@@ -0,0 +1,18 @@
+import Meteor from "@renderer/assets/meteor.svg?react";
+import "./debrid-badge.scss";
+import { useTranslation } from "react-i18next";
+
+export interface DebridBadgeProps {
+ collapsed?: boolean;
+}
+
+export function DebridBadge({ collapsed }: Readonly) {
+ const { t } = useTranslation("hydra_cloud");
+
+ return (
+
+
+ {!collapsed && t("debrid_description")}
+
+ );
+}
diff --git a/src/renderer/src/components/index.ts b/src/renderer/src/components/index.ts
index 65d07440..8373e0dc 100644
--- a/src/renderer/src/components/index.ts
+++ b/src/renderer/src/components/index.ts
@@ -14,3 +14,4 @@ export * from "./toast/toast";
export * from "./badge/badge";
export * from "./confirmation-modal/confirmation-modal";
export * from "./suspense-wrapper/suspense-wrapper";
+export * from "./debrid-badge/debrid-badge";
diff --git a/src/renderer/src/constants.ts b/src/renderer/src/constants.ts
index 3706d7d2..472ed3a7 100644
--- a/src/renderer/src/constants.ts
+++ b/src/renderer/src/constants.ts
@@ -11,6 +11,7 @@ export const DOWNLOADER_NAME = {
[Downloader.Datanodes]: "Datanodes",
[Downloader.Mediafire]: "Mediafire",
[Downloader.TorBox]: "TorBox",
+ [Downloader.Hydra]: "Nimbus",
};
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts
index 791370a2..dd2f24d7 100644
--- a/src/renderer/src/declaration.d.ts
+++ b/src/renderer/src/declaration.d.ts
@@ -59,6 +59,9 @@ declare global {
cb: (value: SeedingStatus[]) => void
) => () => Electron.IpcRenderer;
onHardDelete: (cb: () => void) => () => Electron.IpcRenderer;
+ checkDebridAvailability: (
+ magnets: string[]
+ ) => Promise>;
/* Catalogue */
searchGames: (
diff --git a/src/renderer/src/hooks/use-feature.ts b/src/renderer/src/hooks/use-feature.ts
index 65d0f694..463cfc36 100644
--- a/src/renderer/src/hooks/use-feature.ts
+++ b/src/renderer/src/hooks/use-feature.ts
@@ -1,8 +1,9 @@
-import { useEffect, useState } from "react";
+import { useEffect, useState, useCallback } from "react";
enum Feature {
CheckDownloadWritePermission = "CHECK_DOWNLOAD_WRITE_PERMISSION",
Torbox = "TORBOX",
+ Nimbus = "NIMBUS",
}
export function useFeature() {
@@ -15,14 +16,17 @@ export function useFeature() {
});
}, []);
- const isFeatureEnabled = (feature: Feature) => {
- if (!features) {
- const features = JSON.parse(localStorage.getItem("features") ?? "[]");
- return features.includes(feature);
- }
+ const isFeatureEnabled = useCallback(
+ (feature: Feature) => {
+ if (!features) {
+ const features = JSON.parse(localStorage.getItem("features") ?? "[]");
+ return features.includes(feature);
+ }
- return features.includes(feature);
- };
+ return features.includes(feature);
+ },
+ [features]
+ );
return {
isFeatureEnabled,
diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx
index 33f4b812..fa0be02a 100644
--- a/src/renderer/src/pages/downloads/download-group.tsx
+++ b/src/renderer/src/pages/downloads/download-group.tsx
@@ -374,6 +374,21 @@ export function DownloadGroup({
)}
+
+ {game.download?.downloader === Downloader.Hydra && (
+
+ )}
);
})}
diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx
index 0200609f..c06f280b 100644
--- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx
+++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx
@@ -83,6 +83,10 @@ export function DownloadSettingsModal({
const getDefaultDownloader = useCallback(
(availableDownloaders: Downloader[]) => {
+ if (availableDownloaders.includes(Downloader.Hydra)) {
+ return Downloader.Hydra;
+ }
+
if (availableDownloaders.includes(Downloader.TorBox)) {
return Downloader.TorBox;
}
@@ -110,11 +114,15 @@ export function DownloadSettingsModal({
return userPreferences?.realDebridApiToken;
if (downloader === Downloader.TorBox)
return userPreferences?.torBoxApiToken;
+ if (downloader === Downloader.Hydra)
+ return isFeatureEnabled(Feature.Nimbus);
return true;
});
setSelectedDownloader(getDefaultDownloader(filteredDownloaders));
}, [
+ Feature,
+ isFeatureEnabled,
getDefaultDownloader,
userPreferences?.downloadsPath,
downloaders,
@@ -181,7 +189,9 @@ export function DownloadSettingsModal({
(downloader === Downloader.RealDebrid &&
!userPreferences?.realDebridApiToken) ||
(downloader === Downloader.TorBox &&
- !userPreferences?.torBoxApiToken);
+ !userPreferences?.torBoxApiToken) ||
+ (downloader === Downloader.Hydra &&
+ !isFeatureEnabled(Feature.Nimbus));
return (
+
+ {hashesInDebrid[getHashFromMagnet(repack.uris[0]) ?? ""] && (
+
+ )}
);
})}
diff --git a/src/shared/constants.ts b/src/shared/constants.ts
index 1b3dc9a4..54827ce3 100644
--- a/src/shared/constants.ts
+++ b/src/shared/constants.ts
@@ -7,6 +7,7 @@ export enum Downloader {
Datanodes,
Mediafire,
TorBox,
+ Hydra,
}
export enum DownloadSourceStatus {
@@ -56,6 +57,7 @@ export enum DownloadError {
NotCachedInTorbox = "download_error_not_cached_in_torbox",
GofileQuotaExceeded = "download_error_gofile_quota_exceeded",
RealDebridAccountNotAuthorized = "download_error_real_debrid_account_not_authorized",
+ NotCachedInHydra = "download_error_not_cached_in_hydra",
}
export const FILE_EXTENSIONS_TO_EXTRACT = [".rar", ".zip", ".7z"];
diff --git a/src/shared/index.ts b/src/shared/index.ts
index e679fdac..39b035ff 100644
--- a/src/shared/index.ts
+++ b/src/shared/index.ts
@@ -117,7 +117,12 @@ export const getDownloadersForUri = (uri: string) => {
return [Downloader.RealDebrid];
if (uri.startsWith("magnet:")) {
- return [Downloader.Torrent, Downloader.TorBox, Downloader.RealDebrid];
+ return [
+ Downloader.Torrent,
+ Downloader.Hydra,
+ Downloader.TorBox,
+ Downloader.RealDebrid,
+ ];
}
return [];
diff --git a/src/types/level.types.ts b/src/types/level.types.ts
index 21836870..eb48ced4 100644
--- a/src/types/level.types.ts
+++ b/src/types/level.types.ts
@@ -81,6 +81,7 @@ export interface UserPreferences {
enableAutoInstall?: boolean;
seedAfterDownloadComplete?: boolean;
showHiddenAchievementsDescription?: boolean;
+ showDownloadSpeedInMegabits?: boolean;
downloadNotificationsEnabled?: boolean;
repackUpdatesNotificationsEnabled?: boolean;
achievementNotificationsEnabled?: boolean;