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/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/download/download-manager.ts b/src/main/services/download/download-manager.ts index 35841d33..7401683e 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; @@ -387,6 +388,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..de754947 --- /dev/null +++ b/src/main/services/download/hydra-debrid.ts @@ -0,0 +1,25 @@ +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 getDownloadUrl(magnet: string) { + try { + return HydraApi.post("/debrid/request-file", { + magnet, + }).then((response) => 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..de8e3b75 --- /dev/null +++ b/src/renderer/src/components/debrid-badge/debrid-badge.tsx @@ -0,0 +1,15 @@ +import Meteor from "@renderer/assets/meteor.svg?react"; +import "./debrid-badge.scss"; + +export interface DebridBadgeProps { + collapsed?: boolean; +} + +export function DebridBadge({ collapsed }: Readonly) { + return ( +
+ + {!collapsed && "Baixe até 2x mais rápido com Nimbus"} +
+ ); +} 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/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..d2237439 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; } diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index b3879758..5b225380 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -1,7 +1,13 @@ import { useContext, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Badge, Button, Modal, TextField } from "@renderer/components"; +import { + Badge, + Button, + DebridBadge, + Modal, + TextField, +} from "@renderer/components"; import type { GameRepack } from "@types"; import { DownloadSettingsModal } from "./download-settings-modal"; @@ -31,16 +37,52 @@ export function RepacksModal({ const [repack, setRepack] = useState(null); const [showSelectFolderModal, setShowSelectFolderModal] = useState(false); + const [hashesInDebrid, setHashesInDebrid] = useState>( + {} + ); + const { repacks, game } = useContext(gameDetailsContext); const { t } = useTranslation("game_details"); const { formatDate } = useDate(); - const sortedRepacks = useMemo(() => { - return orderBy(repacks, (repack) => repack.uploadDate, "desc"); + const getHashFromMagnet = (magnet: string) => { + if (!magnet || typeof magnet !== "string") { + return null; + } + + const hashRegex = /xt=urn:btih:([a-zA-Z0-9]+)/i; + const match = magnet.match(hashRegex); + + return match ? match[1].toLowerCase() : null; + }; + + useEffect(() => { + const magnets = repacks.flatMap((repack) => + repack.uris.filter((uri) => uri.startsWith("magnet:")) + ); + + window.electron.checkDebridAvailability(magnets).then((availableHashes) => { + setHashesInDebrid(availableHashes); + }); }, [repacks]); + const sortedRepacks = useMemo(() => { + return orderBy( + repacks, + [ + (repack) => { + const magnet = repack.uris.find((uri) => uri.startsWith("magnet:")); + const hash = magnet ? getHashFromMagnet(magnet) : null; + return hash ? (hashesInDebrid[hash] ?? false) : false; + }, + (repack) => repack.uploadDate, + ], + ["desc", "desc"] + ); + }, [repacks, hashesInDebrid]); + useEffect(() => { setFilteredRepacks(sortedRepacks); }, [sortedRepacks, visible, game]); @@ -110,6 +152,10 @@ export function RepacksModal({ {repack.fileSize} - {repack.repacker} -{" "} {repack.uploadDate ? formatDate(repack.uploadDate) : ""}

+ + {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 0470728c..f5e097bc 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -111,7 +111,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 [];