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 ( ); })} 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;