Compare commits

...

90 Commits
v2.0 ... v2.0.2

Author SHA1 Message Date
Chubby Granny Chaser
05ec01178b Merge pull request #722 from hydralauncher/rc/2.0.2
Rc/2.0.2
2024-06-28 22:15:40 +01:00
Chubby Granny Chaser
84e279cc14 Merge branch 'main' into rc/2.0.2 2024-06-28 22:08:25 +01:00
Chubby Granny Chaser
8eca067aed ci: fix sentry variable 2024-06-28 22:01:42 +01:00
Chubby Granny Chaser
05e4934f9f ci: fix sentry variable 2024-06-28 21:47:27 +01:00
Chubby Granny Chaser
ec0439e41b ci: fix sentry variable 2024-06-28 21:25:03 +01:00
Chubby Granny Chaser
b61fd1e61a Merge pull request #720 from hydralauncher/ci/sentry
ci: adding sentry
2024-06-28 20:55:42 +01:00
Zamitto
6d4f47df38 Merge pull request #566 from Panetina/romanian
Translated to romanian
2024-06-28 16:52:15 -03:00
Zamitto
0eaf629d37 Merge pull request #671 from CMAULTOP/main
Fix ru language
2024-06-28 16:50:45 -03:00
Zamitto
c12f16f59e Merge pull request #718 from hydralauncher/feat/better-api-logs-and-handle-401
feat: better api logs and handle 401
2024-06-28 16:42:42 -03:00
Chubby Granny Chaser
ac27438a35 ci: adding sentry 2024-06-28 20:27:22 +01:00
Zamitto
d3787b4525 feat: remove unused strings 2024-06-28 15:46:22 -03:00
Zamitto
ec8a0f75ac Merge branch 'main' into romanian 2024-06-28 15:42:50 -03:00
Zamitto
7e85ac5b43 feat: rename vbs file 2024-06-28 15:35:12 -03:00
Zamitto
a4644e7501 Update translation.json 2024-06-28 15:20:38 -03:00
Zamitto
ed978af3ae feat: disable old windows auto launch 2024-06-28 13:16:33 -03:00
Zamitto
4bd2174bf3 feat: handling 401 status code 2024-06-28 12:24:12 -03:00
Zamitto
c27182c618 feat: navigate back if request fails for get user 2024-06-28 12:23:46 -03:00
Zamitto
1ceabb00be feat: better logs on api error 2024-06-28 11:29:23 -03:00
Chubby Granny Chaser
2a44313d84 Merge pull request #706 from hydralauncher/feature/libtorrent-reloaded-remake-remaster
Feature/libtorrent reloaded remake remaster
2024-06-28 15:21:45 +01:00
Zamitto
e0dca85825 Merge pull request #709 from hydralauncher/hyd-192-select-lnk-as-parse-target-executable
feat: make it possible to select shortcuts (.lnk) on game executable
2024-06-28 11:16:47 -03:00
Zamitto
ec8ccf7728 Merge pull request #710 from hydralauncher/fix/window-auto-launch-on-startup
fix: windows auto launch on startup
2024-06-28 11:16:32 -03:00
Zamitto
e88088cca4 feat: add new line and rename script file to hydralauncher 2024-06-28 09:55:17 -03:00
Chubby Granny Chaser
75b69f38fc chore: removing extra line on main.py 2024-06-28 13:43:57 +01:00
Chubby Granny Chaser
50a1ba1dea feat: adding file verification message 2024-06-28 13:40:59 +01:00
Chubby Granny Chaser
2229151795 feat: splitting downloader.py 2024-06-28 12:20:09 +01:00
Chubby Granny Chaser
041fce027e feat: splitting downloader.py 2024-06-28 12:08:33 +01:00
Chubby Granny Chaser
1d5004ecb4 Merge branch 'main' into main 2024-06-28 12:04:39 +01:00
Chubby Granny Chaser
363bcf16a4 feat: adding authorization to rpc 2024-06-28 12:03:01 +01:00
Zamitto
b1532a52c8 feat: add script to resources 2024-06-27 19:40:53 -03:00
Zamitto
a3f7d3c59e fix: send signout event when auth token is empty 2024-06-27 19:05:08 -03:00
Zamitto
f1fecb684b feat: dont show auto launch on portable version 2024-06-27 19:00:12 -03:00
Zamitto
9c99e56b70 fix: add script to auto launch hydra on startup 2024-06-27 18:50:18 -03:00
Zamitto
7be626b3dd feat: make it possible to select shortcuts (.lnk) 2024-06-27 18:10:02 -03:00
Chubby Granny Chaser
96e96cd8aa feat: adding real debrid downloads 2024-06-27 22:05:50 +01:00
Chubby Granny Chaser
13644c60e8 feat: adding real debrid downloads 2024-06-27 21:52:04 +01:00
Chubby Granny Chaser
a1e41ea464 feat: adding file verification message 2024-06-27 19:59:33 +01:00
Chubby Granny Chaser
41dc504660 feat: adding initial torrent as arg command 2024-06-27 19:26:04 +01:00
Chubby Granny Chaser
a0cc15b5d8 feat: increasing healthcheck duration 2024-06-27 18:52:53 +01:00
Chubby Granny Chaser
7cd121cb80 feat: adding healthcheck 2024-06-27 18:46:59 +01:00
Chubby Granny Chaser
ccaea88a88 Merge branch 'feature/libtorrent-reloaded-remake-remaster' of github.com:hydralauncher/hydra into feature/libtorrent-reloaded-remake-remaster 2024-06-27 18:12:10 +01:00
Chubby Granny Chaser
d90888c7ba Merge branch 'main' into feature/libtorrent-reloaded-remake-remaster 2024-06-27 18:11:31 +01:00
Chubby Granny Chaser
9f9ea6ee88 fix: removing python tick 2024-06-27 18:10:30 +01:00
Chubby Granny Chaser
c26315219e fix: keeping last status available on rpc 2024-06-27 17:38:20 +01:00
Chubby Granny Chaser
c1c06c2d20 Merge branch 'feature/libtorrent-reloaded-remake-remaster' of github.com:hydralauncher/hydra into feature/libtorrent-reloaded-remake-remaster 2024-06-27 17:19:54 +01:00
Chubby Granny Chaser
328b7cb137 feat: using rpc to communicate 2024-06-27 17:18:48 +01:00
Zamitto
82f72071f9 Merge pull request #707 from hydralauncher/i18n/kazach-translation
feat: add Kazakh translation
2024-06-27 12:25:32 -03:00
Zamitto
d9ed2403ed feat: add kazach translation 2024-06-27 11:30:23 -03:00
Chubby Granny Chaser
d447942f84 Merge branch 'main' into feature/libtorrent-reloaded-remake-remaster 2024-06-27 15:23:48 +01:00
Chubby Granny Chaser
05cfdefc84 fix: fixing postinstall script 2024-06-27 15:21:16 +01:00
Zamitto
e4020d5b6a Merge pull request #605 from Ecron/lang-ca
Added Catalan translation.
2024-06-27 11:13:03 -03:00
Zamitto
1a047547fc feat: remove outdated strings 2024-06-27 11:02:29 -03:00
Chubby Granny Chaser
47ab35421c feat: adding libtorrent again 2024-06-27 14:57:25 +01:00
Chubby Granny Chaser
e08aa9c299 feat: adding libtorrent again 2024-06-27 14:56:57 +01:00
Chubby Granny Chaser
e44049ff63 feat: adding libtorrent again 2024-06-27 14:55:50 +01:00
Zamitto
7aa02f9d64 Merge branch 'main' into lang-ca 2024-06-27 10:55:02 -03:00
Zamitto
3fe6ab469b Fix some comma problems 2024-06-27 10:54:44 -03:00
Chubby Granny Chaser
ccd1d18981 feat: adding libtorrent again 2024-06-27 14:54:02 +01:00
Chubby Granny Chaser
906e801036 feat: adding libtorrent again 2024-06-27 14:52:53 +01:00
Chubby Granny Chaser
63c13e17cb feat: adding libtorrent again 2024-06-27 14:51:13 +01:00
Ecron
c1297530f6 Update translation.json
Added 2 new strings under header section.
2024-06-27 15:42:22 +02:00
Ecron
ac10e755b8 Update src/locales/ca/translation.json
Removed the splash section.
2024-06-27 15:41:10 +02:00
Ecron
1f17dda2f8 Update src/locales/ca/translation.json
Removed social networks.
2024-06-27 15:38:33 +02:00
Zamitto
94284a427f Merge pull request #677 from hydralauncher/fix/captcha-not-showing-on-linux
fix: set nodeIntegrationInSubFrames true on auth window
2024-06-25 10:04:10 -03:00
Павел
7fe8a6425b Update translation.json 2024-06-25 09:28:52 +03:00
Zamitto
2e1eb9e9b7 fix: set nodeIntegrationInSubFrames true on auth window 2024-06-24 23:36:55 -03:00
Zamitto
fe33045b9e Merge pull request #676 from hydralauncher/zamitto/hyd-22-define-the-cloudfront-url-on-the-csp
feat: add cloud front url to CSP
2024-06-24 21:16:57 -03:00
Zamitto
2020663ee5 feat: use wildcard on cloudfront url 2024-06-24 21:09:03 -03:00
Zamitto
2b51b82d03 feat: add cloud front to CSP 2024-06-24 21:02:13 -03:00
Павел
13b691aaad Update translation.json 2024-06-24 20:29:19 +03:00
Павел
e10f9f829c Update translation.json 2024-06-24 20:24:00 +03:00
Павел
936881e570 Update translation.json 2024-06-24 20:18:18 +03:00
Павел
0c826cb6f7 Update translation.json 2024-06-24 20:16:32 +03:00
Zamitto
2a27c37a25 Merge pull request #649 from 01M/patch-1
Update translation.json
2024-06-23 21:40:31 -03:00
Zamitto
3fd9776987 Merge branch 'main' into patch-1 2024-06-23 12:58:54 -03:00
Zamitto
e93b0a786e Merge pull request #638 from Lianela/main
Updated Spanish translation
2024-06-23 12:01:50 -03:00
01M
7a6d8ece63 Merge branch 'main' into patch-1 2024-06-23 17:55:20 +03:00
Zamitto
51c56f7536 Merge branch 'main' into main 2024-06-23 11:51:18 -03:00
Zamitto
87f5e7eb26 Merge pull request #642 from expload233/main
Update Chinese Translation for new version
2024-06-23 11:50:46 -03:00
01M
2a3fda90b3 Update translation.json
Fixed translation
2024-06-23 17:25:54 +03:00
expload
9d11cac680 add new translate text 2024-06-23 06:42:35 +00:00
expload
42209b51a6 Update Chinese translation text for new version 2024-06-23 06:41:11 +00:00
Lianela
170826ad5d Merge branch 'main' into main 2024-06-22 16:18:01 -06:00
Chubby Granny Chaser
11dffd1b7a fix: profiles hotfix 2024-06-22 23:11:32 +01:00
Lianela
37eddbaeeb Updated Spanish translation
- Added and fixed some strings
- Fixed some typo errors
- Translated missing strings

(Had some troubles so I wasn't able to translated before)
2024-06-22 16:04:50 -06:00
Ecron
4744d1ed52 Update index.ts expoting Catalan language file 2024-06-20 17:09:36 +02:00
Ecron
3b40413257 Added Catalan translation.
Added Catalan translation.
2024-06-18 16:46:13 +02:00
Zamitto
42eff5e906 Merge branch 'main' into romanian 2024-06-12 13:35:41 -03:00
Zamitto
d29f266ca1 Merge branch 'main' into romanian 2024-06-10 13:04:01 -03:00
Panetina
42864a4bea Merge branch 'main' into romanian 2024-06-05 12:03:48 +03:00
Panetina
d70b46d475 Translated to romanian
Translated everything to romanian.
Discord: panyel if there are any issues
2024-06-04 15:20:52 +03:00
69 changed files with 3744 additions and 1464 deletions

View File

@@ -1,3 +1,3 @@
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
MAIN_VITE_API_URL=API_URL MAIN_VITE_API_URL=API_URL
MAIN_VITE_SENTRY_DSN=YOUR_SENTRY_DSN

View File

@@ -22,6 +22,17 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Install dependencies
run: pip install -r requirements.txt
- name: Build with cx_Freeze
run: python torrent-client/setup.py build
- name: Build Linux - name: Build Linux
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
run: yarn build:linux run: yarn build:linux
@@ -29,6 +40,8 @@ jobs:
MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }} MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }} MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows - name: Build Windows
@@ -38,6 +51,8 @@ jobs:
MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }} MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }} MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create artifact - name: Create artifact

View File

@@ -1,6 +1,6 @@
name: Lint name: Lint
on: [pull_request, push] on: pull_request
jobs: jobs:
lint: lint:

View File

@@ -24,12 +24,26 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Install dependencies
run: pip install -r requirements.txt
- name: Build with cx_Freeze
run: python torrent-client/setup.py build
- name: Build Linux - name: Build Linux
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
run: yarn build:linux run: yarn build:linux
env: env:
MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }} MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }} MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows - name: Build Windows
@@ -38,6 +52,9 @@ jobs:
env: env:
MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }} MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }} MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Release - name: Release

3
.gitignore vendored
View File

@@ -1,6 +1,6 @@
.vscode .vscode
node_modules node_modules
aria2/ hydra-download-manager/
fastlist.exe fastlist.exe
__pycache__ __pycache__
dist dist
@@ -9,3 +9,4 @@ out
*.log* *.log*
.env .env
.vite .vite
sentry.properties

View File

@@ -3,11 +3,12 @@ productName: Hydra
directories: directories:
buildResources: build buildResources: build
extraResources: extraResources:
- aria2 - hydra-download-manager
- seeds - seeds
- from: node_modules/ps-list/vendor/fastlist-0.3.0-x64.exe - from: node_modules/ps-list/vendor/fastlist-0.3.0-x64.exe
to: fastlist.exe to: fastlist.exe
- from: node_modules/create-desktop-shortcuts/src/windows.vbs - from: node_modules/create-desktop-shortcuts/src/windows.vbs
- from: resources/hydralauncher.vbs
files: files:
- "!**/.vscode/*" - "!**/.vscode/*"
- "!src/*" - "!src/*"

View File

@@ -6,9 +6,16 @@ import {
externalizeDepsPlugin, externalizeDepsPlugin,
} from "electron-vite"; } from "electron-vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { sentryVitePlugin } from "@sentry/vite-plugin";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin"; import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import svgr from "vite-plugin-svgr"; import svgr from "vite-plugin-svgr";
const sentryPlugin = sentryVitePlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: "hydra-launcher",
project: "hydra-launcher",
});
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
loadEnv(mode); loadEnv(mode);
@@ -28,7 +35,7 @@ export default defineConfig(({ mode }) => {
"@shared": resolve("src/shared"), "@shared": resolve("src/shared"),
}, },
}, },
plugins: [externalizeDepsPlugin(), swcPlugin()], plugins: [externalizeDepsPlugin(), swcPlugin(), sentryPlugin],
}, },
preload: { preload: {
plugins: [externalizeDepsPlugin()], plugins: [externalizeDepsPlugin()],
@@ -44,7 +51,7 @@ export default defineConfig(({ mode }) => {
"@shared": resolve("src/shared"), "@shared": resolve("src/shared"),
}, },
}, },
plugins: [svgr(), react(), vanillaExtractPlugin()], plugins: [svgr(), react(), vanillaExtractPlugin(), sentryPlugin],
}, },
}; };
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "hydralauncher", "name": "hydralauncher",
"version": "2.0.0", "version": "2.0.2",
"description": "Hydra", "description": "Hydra",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "Los Broxas", "author": "Los Broxas",
@@ -23,7 +23,7 @@
"start": "electron-vite preview", "start": "electron-vite preview",
"dev": "electron-vite dev", "dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build", "build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps && node ./postinstall.cjs", "postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir", "build:unpack": "npm run build && electron-builder --dir",
"build:win": "electron-vite build && electron-builder --win", "build:win": "electron-vite build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac", "build:mac": "electron-vite build && electron-builder --mac",
@@ -38,9 +38,9 @@
"@fontsource/fira-sans": "^5.0.20", "@fontsource/fira-sans": "^5.0.20",
"@primer/octicons-react": "^19.9.0", "@primer/octicons-react": "^19.9.0",
"@reduxjs/toolkit": "^2.2.3", "@reduxjs/toolkit": "^2.2.3",
"@sentry/electron": "^5.1.0",
"@vanilla-extract/css": "^1.14.2", "@vanilla-extract/css": "^1.14.2",
"@vanilla-extract/recipes": "^0.5.2", "@vanilla-extract/recipes": "^0.5.2",
"aria2": "^4.1.2",
"auto-launch": "^5.0.6", "auto-launch": "^5.0.6",
"axios": "^1.6.8", "axios": "^1.6.8",
"better-sqlite3": "^9.5.0", "better-sqlite3": "^9.5.0",
@@ -81,6 +81,7 @@
"@electron-toolkit/eslint-config-prettier": "^2.0.0", "@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^1.0.1", "@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@sentry/vite-plugin": "^2.20.1",
"@swc/core": "^1.4.16", "@swc/core": "^1.4.16",
"@types/auto-launch": "^5.0.5", "@types/auto-launch": "^5.0.5",
"@types/color": "^3.0.6", "@types/color": "^3.0.6",

View File

@@ -1,50 +0,0 @@
const { default: axios } = require("axios");
const util = require("node:util");
const fs = require("node:fs");
const exec = util.promisify(require("node:child_process").exec);
const downloadAria2 = 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);
});
};
downloadAria2();

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
libtorrent
cx_Freeze
cx_Logging; sys_platform == 'win32'
lief; sys_platform == 'win32'
pywin32; sys_platform == 'win32'

View File

@@ -0,0 +1,3 @@
Set WshShell = CreateObject("WScript.Shell" )
WshShell.Run """%localappdata%\Programs\Hydra\Hydra.exe""", 0 'Must quote command if it has spaces; must escape quotes
Set WshShell = Nothing

View File

@@ -0,0 +1,148 @@
{
"home": {
"featured": "Destacats",
"trending": "Populars",
"surprise_me": "Sorprèn-me",
"no_results": "No s'ha trobat res"
},
"sidebar": {
"catalogue": "Catàleg",
"downloads": "Baixades",
"settings": "Configuració",
"my_library": "Biblioteca",
"downloading_metadata": "{{title}} (S'estan baixant les metadades…)",
"paused": "{{title}} (Pausat)",
"downloading": "{{title}} ({{percentage}} - S'està baixant…)",
"filter": "Filtra la biblioteca",
"home": "Inici"
},
"header": {
"search": "Cerca jocs",
"home": "Inici",
"catalogue": "Catàleg",
"downloads": "Baixades",
"search_results": "Resultats de la cerca",
"settings": "Configuració",
"version_available_install": "Hi ha disponible la versió {{version}}. Feu clic aquí per a reiniciar i instal·lar-la.",
"version_available_download": "Hi ha disponible la versió {{version}}. Feu clic aquí per a baixar-la."
},
"bottom_panel": {
"no_downloads_in_progress": "Cap baixada en curs",
"downloading_metadata": "S'estan baixant les metadades de: {{title}}…",
"downloading": "S'està baixant: {{title}}… ({{percentage}} complet) - Finalització: {{eta}} - {{speed}}"
},
"catalogue": {
"next_page": "Pàgina següent",
"previous_page": "Pàgina anterior"
},
"game_details": {
"open_download_options": "Obre les opcions de baixada",
"download_options_zero": "No hi ha opcions de baixada",
"download_options_one": "{{count}} opció de baixada",
"download_options_other": "{{count}} opcions de baixada",
"updated_at": "Actualitzat: {{updated_at}}",
"install": "Instal·la",
"resume": "Reprèn",
"pause": "Pausa",
"cancel": "Cancel·la",
"remove": "Elimina",
"space_left_on_disk": "{{space}} lliures al disc",
"eta": "Finalització: {{eta}}",
"downloading_metadata": "S'estan baixant les metadades…",
"filter": "Filtra els reempaquetats",
"requirements": "Requisits del sistema",
"minimum": "Mínims",
"recommended": "Recomanats",
"release_date": "Publicat el {{date}}",
"publisher": "Publicat per {{publisher}}",
"hours": "hores",
"minutes": "minuts",
"amount_hours": "{{amount}} hores",
"amount_minutes": "{{amount}} minuts",
"accuracy": "{{accuracy}}% de precisió",
"add_to_library": "Afegeix a la biblioteca",
"remove_from_library": "Elimina de la biblioteca",
"no_downloads": "No hi ha baixades disponibles",
"play_time": "Jugat durant {{amount}}",
"last_time_played": "Última partida: {{period}}",
"not_played_yet": "Encara no has jugat al {{title}}",
"next_suggestion": "Suggeriment següent",
"play": "Inicia",
"deleting": "S'està eliminant l'instal·lador…",
"close": "Tanca",
"playing_now": "S'està jugant",
"change": "Canvia",
"repacks_modal_description": "Tria quin reempaquetat vols baixar",
"select_folder_hint": "Per a canviar la carpeta predefinida, vés a la <0>Configuració</0>",
"download_now": "Baixa ara",
"no_shop_details": "No s'han pogut recuperar els detalls de la tenda.",
"download_options": "Opcions de baixada",
"download_path": "Ruta de baixada",
"previous_screenshot": "Captura anterior",
"next_screenshot": "Captura següent",
"screenshot": "Captura {{number}}",
"open_screenshot": "Obre la captura {{number}}"
},
"activation": {
"title": "Activa l'Hydra",
"installation_id": "ID d'instal·lació:",
"enter_activation_code": "Introdueix el codi d'activació",
"message": "Si no saps on demanar-ho, no ho hauries de tenir.",
"activate": "Activa",
"loading": "S'està carregant…"
},
"downloads": {
"resume": "Reprèn",
"pause": "Pausa",
"eta": "Finalització {{eta}}",
"paused": "Pausada",
"verifying": "S'està verificant…",
"completed": "Completada",
"cancel": "Cancel·la",
"filter": "Filtra els jocs baixats",
"remove": "Elimina",
"downloading_metadata": "S'estan baixant les metadades…",
"deleting": "S'està eliminant l'instal·lador…",
"delete": "Elimina l'instal·lador",
"delete_modal_title": "N'estàs segur?",
"delete_modal_description": "S'eliminaran de l'ordinador tots els fitxers d'instal·lació",
"install": "Instal·la"
},
"settings": {
"downloads_path": "Ruta de baixades",
"change": "Actualitza",
"notifications": "Notificacions",
"enable_download_notifications": "Quan finalitzi una baixada",
"enable_repack_list_notifications": "Quan s'afegeixi un nou reempaquetat",
"real_debrid_api_token_label": "Testimoni de l'API de Real Debrid",
"quit_app_instead_hiding": "Tanca l'Hydra en compte de minimitzar-la a la safata",
"launch_with_system": "Inicia l'Hydra quan s'iniciï el sistema",
"general": "General",
"behavior": "Comportament",
"enable_real_debrid": "Activa el Real Debrid",
"real_debrid_api_token_hint": "Pots obtenir la teva clau de l'API <0>aquí</0>.",
"save_changes": "Desa els canvis"
},
"notifications": {
"download_complete": "La baixada ha finalitzat",
"game_ready_to_install": "{{title}} ja es pot instal·lar",
"repack_list_updated": "S'ha actualitzat la llista de reempaquetats",
"repack_count_one": "S'ha afegit {{count}} reempaquetat",
"repack_count_other": "S'han afegit {{count}} reempaquetats"
},
"system_tray": {
"open": "Obre l'Hydra",
"quit": "Tanca"
},
"game_card": {
"no_downloads": "No hi ha baixades disponibles"
},
"binary_not_found_modal": {
"title": "Programes no instal·lats",
"description": "No s'ha trobat els executables del Wine o el Lutris al sistema.",
"instructions": "Comprova quina és la manera correcta d'instal·lar qualsevol d'ells en la teva distribució de Linux perquè el joc pugui executar-se amb normalitat."
},
"modal": {
"close": "Botó de tancar"
}
}

View File

@@ -36,7 +36,8 @@
"no_downloads_in_progress": "No downloads in progress", "no_downloads_in_progress": "No downloads in progress",
"downloading_metadata": "Downloading {{title}} metadata…", "downloading_metadata": "Downloading {{title}} metadata…",
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}", "downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}",
"calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…" "calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…",
"checking_files": "Checking {{title}} files… ({{percentage}} complete)"
}, },
"catalogue": { "catalogue": {
"next_page": "Next page", "next_page": "Next page",
@@ -144,7 +145,8 @@
"downloads_completed": "Completed", "downloads_completed": "Completed",
"queued": "Queued", "queued": "Queued",
"no_downloads_title": "Such empty", "no_downloads_title": "Such empty",
"no_downloads_description": "You haven't downloaded anything with Hydra yet, but it's never too late to start." "no_downloads_description": "You haven't downloaded anything with Hydra yet, but it's never too late to start.",
"checking_files": "Checking files…"
}, },
"settings": { "settings": {
"downloads_path": "Downloads path", "downloads_path": "Downloads path",

View File

@@ -1,6 +1,6 @@
{ {
"app": { "app": {
"successfully_signed_in": "Successfully signed in (TRANSLATE ME)" "successfully_signed_in": "Sesión iniciada correctamente"
}, },
"home": { "home": {
"featured": "Destacado", "featured": "Destacado",
@@ -20,7 +20,7 @@
"home": "Inicio", "home": "Inicio",
"queued": "{{title}} (En Cola)", "queued": "{{title}} (En Cola)",
"game_has_no_executable": "El juego no tiene un ejecutable", "game_has_no_executable": "El juego no tiene un ejecutable",
"sign_in": "Sign in (TRANSLATE ME)" "sign_in": "Iniciar sesión"
}, },
"header": { "header": {
"search": "Buscar juegos", "search": "Buscar juegos",
@@ -110,7 +110,9 @@
"danger_zone_section_description": "Eliminar este juego de tu librería o los archivos descargados por Hydra", "danger_zone_section_description": "Eliminar este juego de tu librería o los archivos descargados por Hydra",
"download_in_progress": "Descarga en progreso", "download_in_progress": "Descarga en progreso",
"download_paused": "Descarga pausada", "download_paused": "Descarga pausada",
"last_downloaded_option": "Última opción descargada" "last_downloaded_option": "Última opción descargada",
"create_shortcut_success": "Atajo creado con éxito",
"create_shortcut_error": "Error al crear un atajo"
}, },
"activation": { "activation": {
"title": "Activar Hydra", "title": "Activar Hydra",
@@ -183,12 +185,12 @@
"sync_download_sources": "Sincronizar fuentes", "sync_download_sources": "Sincronizar fuentes",
"removed_download_source": "Fuente de descarga eliminada", "removed_download_source": "Fuente de descarga eliminada",
"added_download_source": "Fuente de descarga añadida", "added_download_source": "Fuente de descarga añadida",
"download_sources_synced": "Todas las fuentes de descarga estánn actualizadas (TRANSLATE ME)", "download_sources_synced": "Todas las fuentes de descargas están actualizadas.",
"insert_valid_json_url": "Insert a valid JSON url (TRANSLATE ME)", "insert_valid_json_url": "Introduce una URL JSON válida",
"found_download_option_zero": "No download option found (TRANSLATE ME)", "found_download_option_zero": "No se encontró una opción de descarga",
"found_download_option_one": "Found {{countFormatted}} download option (TRANSLATE ME)", "found_download_option_one": "Se encontró {{countFormatted}} opción de descarga",
"found_download_option_other": "Found {{countFormatted}} download options (TRANSLATE ME)", "found_download_option_other": "Se encontraron {{countFormatted}} opciones de descarga",
"import": "Import (TRANSLATE ME)" "import": "Importar"
}, },
"notifications": { "notifications": {
"download_complete": "Descarga completada", "download_complete": "Descarga completada",
@@ -216,25 +218,25 @@
"toggle_password_visibility": "Cambiar visibilidad de contraseña" "toggle_password_visibility": "Cambiar visibilidad de contraseña"
}, },
"user_profile": { "user_profile": {
"amount_hours": "{{amount}} hours (TRANSLATE ME)", "amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutes (TRANSLATE ME)", "amount_minutes": "{{amount}} minutos",
"last_time_played": "Last played {{period}} (TRANSLATE ME)", "last_time_played": "Última vez jugado {{period}}",
"activity": "Recent activity (TRANSLATE ME)", "activity": "Actividad reciente",
"library": "Library (TRANSLATE ME)", "library": "Biblioteca",
"total_play_time": "Total playtime: {{amount}} (TRANSLATE ME)", "total_play_time": "Total de tiempo jugado: {{amount}}",
"no_recent_activity_title": "Hmmm… nothing here (TRANSLATE ME)", "no_recent_activity_title": "Que raro, no hay nada por acá, ¿que tal si jugamos algo para empezar?",
"no_recent_activity_description": "You haven't played any games recently. It's time to change that! (TRANSLATE ME)", "no_recent_activity_description": "No has jugado ningún juego recientemente, ¡vamos a cambiar eso ahora!",
"display_name": "Display name (TRANSLATE ME)", "display_name": "Nombre a mostrar",
"saving": "Saving (TRANSLATE ME)", "saving": "Guardando",
"save": "Save (TRANSLATE ME)", "save": "Guardar",
"edit_profile": "Edit Profile (TRANSLATE ME)", "edit_profile": "Editar perfil",
"saved_successfully": "Saved successfully (TRANSLATE ME)", "saved_successfully": "Guardado exitosamente",
"try_again": "Please, try again (TRANSLATE ME)", "try_again": "Por favor, intenta de nuevo",
"sign_out_modal_title": "Are you sure? (TRANSLATE ME)", "sign_out_modal_title": "¿Estás seguro?",
"cancel": "Cancel (TRANSLATE ME)", "cancel": "Cancelar",
"successfully_signed_out": "Successfully signed out (TRANSLATE ME)", "successfully_signed_out": "Sesión cerrada exitosamente",
"sign_out": "Sign out (TRANSLATE ME)", "sign_out": "Cerrar sesión",
"playing_for": "Playing for {{amount}} (TRANSLATE ME)", "playing_for": "Jugando por {{amount}}",
"sign_out_modal_text": "Your library is linked with your current account. When signing out, your library will not be visible anymore, and any progress will not be saved. Continue with sign out? (TRANSLATE ME)" "sign_out_modal_text": "Tu biblioteca se ha vinculado con tu cuenta. Cuando cierres sesión, tú biblioteca ya no será visible y cualquier progreso no se guardará. ¿Continuar con el cierre de sesión?"
} }
} }

View File

@@ -16,3 +16,6 @@ export { default as ko } from "./ko/translation.json";
export { default as da } from "./da/translation.json"; export { default as da } from "./da/translation.json";
export { default as ar } from "./ar/translation.json"; export { default as ar } from "./ar/translation.json";
export { default as fa } from "./fa/translation.json"; export { default as fa } from "./fa/translation.json";
export { default as ro } from "./ro/translation.json";
export { default as ca } from "./ca/translation.json";
export { default as kk } from "./kk/translation.json";

View File

@@ -0,0 +1,242 @@
{
"app": {
"successfully_signed_in": "Сәтті кіру"
},
"home": {
"featured": "Ұсынылған",
"trending": "Трендте",
"surprise_me": "Таңқалдыр",
"no_results": "Ештеңе табылмады"
},
"sidebar": {
"catalogue": "Каталог",
"downloads": "Жүктеулер",
"settings": "Параметрлер",
"my_library": "Кітапхана",
"downloading_metadata": "{{title}} (Метадеректерді жүктеу…)",
"paused": "{{title}} (Тоқтатылды)",
"downloading": "{{title}} ({{percentage}} - Жүктеу…)",
"filter": "Кітапхана фильтрі",
"home": "Басты бет",
"queued": "{{title}} (Кезекте)",
"game_has_no_executable": "Ойынды іске қосу файлы таңдалмаған",
"sign_in": "Кіру"
},
"header": {
"search": "Іздеу",
"home": "Басты бет",
"catalogue": "Каталог",
"downloads": "Жүктеулер",
"search_results": "Іздеу нәтижелері",
"settings": "Параметрлер",
"version_available_install": "Қол жетімді нұсқа {{version}}. Қайта іске қосу және орнату үшін мұнда басыңыз.",
"version_available_download": "Қол жетімді нұсқа {{version}}. Жүктеу үшін мұнда басыңыз."
},
"bottom_panel": {
"no_downloads_in_progress": "Белсенді жүктеулер жоқ",
"downloading_metadata": "Метадеректерді жүктеу {{title}}…",
"downloading": "Жүктеу {{title}}… ({{percentage}} аяқталды) - Аяқтау {{eta}} - {{speed}}",
"calculating_eta": "Жүктеу {{title}}… ({{percentage}} аяқталды) - Қалған уақытты есептеу…"
},
"catalogue": {
"next_page": "Келесі бет",
"previous_page": "Алдыңғы бет"
},
"game_details": {
"open_download_options": "Жүктеу нұсқаларын ашу",
"download_options_zero": "Жүктеу нұсқалары жоқ",
"download_options_one": "{{count}} жүктеу нұсқасы",
"download_options_other": "{{count}} жүктеу нұсқалары",
"updated_at": "Жаңартылды {{updated_at}}",
"install": "Орнату",
"resume": "Жандандыру",
"pause": "Тоқтату",
"cancel": "Болдырмау",
"remove": "Жою",
"space_left_on_disk": "{{space}} бос орын",
"eta": "Аяқтау {{eta}}",
"calculating_eta": "Қалған уақытты есептеу…",
"downloading_metadata": "Метадеректерді жүктеу…",
"filter": "Репактар фильтрі",
"requirements": "Жүйелік талаптар",
"minimum": "Минималды",
"recommended": "Ұсынылған",
"paused": "Тоқтатылды",
"release_date": "Шыққан күні {{date}}",
"publisher": "Баспагер {{publisher}}",
"hours": "сағат",
"minutes": "минут",
"amount_hours": "{{amount}} сағат",
"amount_minutes": "{{amount}} минут",
"accuracy": "дәлдік {{accuracy}}%",
"add_to_library": "Кітапханаға қосу",
"remove_from_library": "Кітапханадан жою",
"no_downloads": "Жүктеулер жоқ",
"play_time": "Ойнау уақыты {{amount}}",
"last_time_played": "Соңғы ойнаған уақыт {{period}}",
"not_played_yet": "Сіз {{title}} ойнамағансыз",
"next_suggestion": "Келесі ұсыныс",
"play": "Ойнау",
"deleting": "Орнатушыны жою…",
"close": "Жабу",
"playing_now": "Қазір ойнап жатыр",
"change": "Өзгерту",
"repacks_modal_description": "Жүктеу үшін репакты таңдаңыз",
"select_folder_hint": "Әдепкі жүктеу қалтасын өзгерту үшін <0>Параметрлер</0> ашыңыз",
"download_now": "Қазір жүктеу",
"no_shop_details": "Сипаттаманы алу мүмкін болмады",
"download_options": "Жүктеу нұсқалары",
"download_path": "Жүктеу жолы",
"previous_screenshot": "Алдыңғы скриншот",
"next_screenshot": "Келесі скриншот",
"screenshot": "Скриншот {{number}}",
"open_screenshot": "Скриншотты ашу {{number}}",
"download_settings": "Жүктеу параметрлері",
"downloader": "Жүктегіш",
"select_executable": "Таңдау",
"no_executable_selected": "Файл таңдалмаған",
"open_folder": "Қалтаны ашу",
"open_download_location": "Жүктеу қалтасын қарау",
"create_shortcut": "Жұмыс үстелінде жарлық жасау",
"remove_files": "Файлдарды жою",
"remove_from_library_title": "Сіз сенімдісіз бе?",
"remove_from_library_description": "{{game}} сіздің кітапханаңыздан жойылады.",
"options": "Параметрлер",
"executable_section_title": "Файл",
"executable_section_description": "\"Ойнау\" батырмасын басқанда іске қосылатын файл жолы",
"downloads_secion_title": "Жүктеулер",
"downloads_section_description": "Ойынның жаңартулары немесе басқа нұсқалары бар-жоғын тексеру",
"danger_zone_section_title": "Қауіпті аймақ",
"danger_zone_section_description": "Осы ойынды кітапханаңыздан жою немесе Hydra жүктеген файлдарды жою",
"download_in_progress": "Жүктеу жүріп жатыр",
"download_paused": "Жүктеу тоқтатылды",
"last_downloaded_option": "Соңғы жүктеу нұсқасы",
"create_shortcut_success": "Жарлық жасалды",
"create_shortcut_error": "Жарлық жасау мүмкін болмады"
},
"activation": {
"title": "Hydra-ны белсендіру",
"installation_id": "Орнату ID:",
"enter_activation_code": "Активтендіру кодын енгізіңіз",
"message": "Егер оның қайдан алуға болатынын білмесеңіз, сізде оның болмауы керек.",
"activate": "Белсендіру",
"loading": "Жүктеу…"
},
"downloads": {
"resume": "Жандандыру",
"pause": "Тоқтату",
"eta": "Аяқтау {{eta}}",
"paused": "Тоқтатылды",
"verifying": "Тексеру…",
"completed": "Аяқталды",
"removed": "Жүктелмеген",
"cancel": "Болдырмау",
"filter": "Жүктелген ойындар фильтрі",
"remove": "Жою",
"downloading_metadata": "Метадеректерді жүктеу…",
"deleting": "Орнатушыны жою…",
"delete": "Орнатушыны жою",
"delete_modal_title": "Сіз сенімдісіз бе?",
"delete_modal_description": "Бұл барлық орнатушыларды компьютеріңізден жояды",
"install": "Орнату",
"download_in_progress": "Жүктеу жүріп жатыр",
"queued_downloads": "Кезектегі жүктеулер",
"downloads_completed": "Аяқталды",
"queued": "Кезекте",
"no_downloads_title": "Мұнда бос...",
"no_downloads_description": "Сіз Hydra арқылы әлі ештеңе жүктемегенсіз, бірақ бастау ешқашан кеш емес."
},
"settings": {
"downloads_path": "Жүктеу жолы",
"change": "Өзгерту",
"notifications": "Хабарламалар",
"enable_download_notifications": "Жүктеу аяқталғанда",
"enable_repack_list_notifications": "Жаңа репак қосылғанда",
"real_debrid_api_token_label": "Real-Debrid API-токен",
"quit_app_instead_hiding": "Hydra-ны трейге жасырудың орнына жабу",
"launch_with_system": "Жүйемен бірге Hydra-ны іске қосу",
"general": "Жалпы",
"behavior": "Мінез-құлық",
"download_sources": "Жүктеу көздері",
"language": "Тіл",
"real_debrid_api_token": "API Кілті",
"enable_real_debrid": "Real-Debrid-ті қосу",
"real_debrid_description": "Real-Debrid - бұл шектеусіз жүктеуші, ол интернетте орналастырылған файлдарды тез жүктеуге немесе жеке желі арқылы кез келген блоктарды айналып өтіп, оларды бірден плеерге беруге мүмкіндік береді.",
"real_debrid_invalid_token": "Қате API кілті",
"real_debrid_api_token_hint": "API кілтін <0>осы жерден</0> алуға болады",
"real_debrid_free_account_error": "\"{{username}}\" аккаунты жазылымға ие емес. Real-Debrid жазылымын алыңыз",
"real_debrid_linked_message": "\"{{username}}\" аккаунты байланған",
"save_changes": "Өзгерістерді сақтау",
"changes_saved": "Өзгерістер сәтті сақталды",
"download_sources_description": "Hydra осы көздерден жүктеу сілтемелерін алады. URL-да жүктеу сілтемелері бар .json файлына тікелей сілтеме болуы керек.",
"validate_download_source": "Тексеру",
"remove_download_source": "Жою",
"add_download_source": "Жүктеу көзін қосу",
"download_count_zero": "Жүктеулер тізімінде жоқ",
"download_count_one": "{{countFormatted}} жүктеу тізімде",
"download_count_other": "{{countFormatted}} жүктеу тізімде",
"download_options_zero": "Қолжетімді жүктеулер жоқ",
"download_options_one": "{{countFormatted}} жүктеу нұсқасы қол жетімді",
"download_options_other": "{{countFormatted}} жүктеу нұсқалары қол жетімді",
"download_source_url": "Көздің сілтемесі",
"add_download_source_description": ".json файлға сілтемені қойыңыз",
"download_source_up_to_date": "Жаңартылған",
"download_source_errored": "Қате",
"sync_download_sources": "Көздерді синхрондау",
"removed_download_source": "Жүктеу көзі жойылды",
"added_download_source": "Жүктеу көзі қосылды",
"download_sources_synced": "Барлық жүктеу көздері синхрондалды",
"insert_valid_json_url": "Жарамды JSON URL енгізіңіз",
"found_download_option_zero": "Жүктеу нұсқалары табылмады",
"found_download_option_one": "{{countFormatted}} жүктеу нұсқасы табылды",
"found_download_option_other": "{{countFormatted}} жүктеу нұсқалары табылды",
"import": "Импорттау"
},
"notifications": {
"download_complete": "Жүктеу аяқталды",
"game_ready_to_install": "{{title}} орнатуға дайын",
"repack_list_updated": "Репактар тізімі жаңартылды",
"repack_count_one": "{{count}} репак қосылды",
"repack_count_other": "{{count}} репактар қосылды"
},
"system_tray": {
"open": "Hydra-ны ашу",
"quit": "Шығу"
},
"game_card": {
"no_downloads": "Жүктеулер жоқ"
},
"binary_not_found_modal": {
"title": "Бағдарламалар орнатылмаған",
"description": "Wine немесе Lutris табылмады",
"instructions": "Linux дистрибутивіңізге олардың кез келгенін дұрыс орнатудың жолын біліңіз осылайша ойын дұрыс жұмыс істей алады"
},
"modal": {
"close": "Жабу"
},
"forms": {
"toggle_password_visibility": "Құпиясөзді көрсету"
},
"user_profile": {
"amount_hours": "{{amount}} сағат",
"amount_minutes": "{{amount}} минут",
"last_time_played": "Соңғы ойын {{period}}",
"activity": "Соңғы әрекет",
"library": "Кітапхана",
"total_play_time": "Барлығы ойнаған: {{amount}}",
"no_recent_activity_title": "Хммм... Мұнда ештеңе жоқ",
"no_recent_activity_description": "Сіз ұзақ уақыт бойы ештеңе ойнаған жоқсыз. Мұны өзгерту керек!",
"display_name": "Көрсету аты",
"saving": "Сақтау",
"save": "Сақталды",
"edit_profile": "Профильді өзгерту",
"saved_successfully": "Сәтті сақталды",
"try_again": "Қайта көріңіз",
"sign_out_modal_title": "Сіз сенімдісіз бе?",
"cancel": "Болдырмау",
"successfully_signed_out": "Аккаунттан сәтті шығу",
"sign_out": "Шығу",
"playing_for": "Ойнаған {{amount}}",
"sign_out_modal_text": "Сіздің кітапханаңыз ағымдағы аккаунтпен байланысты. Жүйеден шыққанда сіздің кітапханаңыз қол жетімсіз болады және прогресс сақталмайды. Шығу?"
}
}

View File

@@ -36,7 +36,8 @@
"no_downloads_in_progress": "Sem downloads em andamento", "no_downloads_in_progress": "Sem downloads em andamento",
"downloading_metadata": "Baixando metadados de {{title}}…", "downloading_metadata": "Baixando metadados de {{title}}…",
"downloading": "Baixando {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}", "downloading": "Baixando {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}",
"calculating_eta": "Baixando {{title}}… ({{percentage}} concluído) - Calculando tempo restante…" "calculating_eta": "Baixando {{title}}… ({{percentage}} concluído) - Calculando tempo restante…",
"checking_files": "Verificando arquivos de {{title}}…"
}, },
"game_details": { "game_details": {
"open_download_options": "Ver opções de download", "open_download_options": "Ver opções de download",
@@ -140,7 +141,8 @@
"downloads_completed": "Completo", "downloads_completed": "Completo",
"queued": "Na fila", "queued": "Na fila",
"no_downloads_title": "Nada por aqui…", "no_downloads_title": "Nada por aqui…",
"no_downloads_description": "Você ainda não baixou nada pelo Hydra, mas nunca é tarde para começar." "no_downloads_description": "Você ainda não baixou nada pelo Hydra, mas nunca é tarde para começar.",
"checking_files": "Verificando arquivos…"
}, },
"settings": { "settings": {
"downloads_path": "Diretório dos downloads", "downloads_path": "Diretório dos downloads",

View File

@@ -0,0 +1,159 @@
{
"home": {
"featured": "Recomandate",
"trending": "Populare",
"surprise_me": "Surprinde-mă",
"no_results": "Niciun rezultat găsit"
},
"sidebar": {
"catalogue": "Catalog",
"downloads": "Descărcări",
"settings": "Setări",
"my_library": "Biblioteca mea",
"downloading_metadata": "{{title}} (Se descarcă metadata...)",
"paused": "{{title}} (Pauzat)",
"downloading": "{{title}} ({{percentage}} - Se descarcă...)",
"filter": "Filtrează biblioteca",
"home": "Acasă"
},
"header": {
"search": "Caută jocuri",
"home": "Acasă",
"catalogue": "Catalog",
"downloads": "Descărcări",
"search_results": "Rezultatele căutării",
"settings": "Setări"
},
"bottom_panel": {
"no_downloads_in_progress": "Nicio descărcare în curs",
"downloading_metadata": "Se descarcă metadata pentru {{title}}...",
"downloading": "Se descarcă {{title}}... ({{percentage}} complet) - Concluzie {{eta}} - {{speed}}",
"calculating_eta": "Se descarcă {{title}}... ({{percentage}} complet) - Calculare timp rămas..."
},
"catalogue": {
"next_page": "Pagina următoare",
"previous_page": "Pagina anterioară"
},
"game_details": {
"open_download_options": "Deschide opțiunile de descărcare",
"download_options_zero": "Nicio opțiune de descărcare",
"download_options_one": "{{count}} opțiune de descărcare",
"download_options_other": "{{count}} opțiuni de descărcare",
"updated_at": "Actualizat la {{updated_at}}",
"install": "Instalează",
"resume": "Reia",
"pause": "Pauză",
"cancel": "Anulează",
"remove": "Elimină",
"space_left_on_disk": "{{space}} liber pe disc",
"eta": "Concluzie {{eta}}",
"calculating_eta": "Calculare timp rămas...",
"downloading_metadata": "Se descarcă metadata...",
"filter": "Filtrează repack-urile",
"requirements": "Cerințe de sistem",
"minimum": "Minim",
"recommended": "Recomandat",
"paused": "Pauzat",
"release_date": "Lansat pe {{date}}",
"publisher": "Publicat de {{publisher}}",
"hours": "ore",
"minutes": "minute",
"amount_hours": "{{amount}} ore",
"amount_minutes": "{{amount}} minute",
"accuracy": "{{accuracy}}% acuratețe",
"add_to_library": "Adaugă în bibliotecă",
"remove_from_library": "Elimină din bibliotecă",
"no_downloads": "Nicio descărcare disponibilă",
"play_time": "Jucat timp de {{amount}}",
"last_time_played": "Ultima dată jucat {{period}}",
"not_played_yet": "Nu ai jucat încă {{title}}",
"next_suggestion": "Sugestia următoare",
"play": "Joacă",
"deleting": "Se șterge programul de instalare...",
"close": "Închide",
"playing_now": "Se joacă acum",
"change": "Schimbă",
"repacks_modal_description": "Alege repack-ul pe care vrei să-l descarci",
"select_folder_hint": "Pentru a schimba folderul predefinit, mergi la <0>Setări</0>",
"download_now": "Descarcă acum",
"no_shop_details": "Nu s-au putut obține detalii din magazin.",
"download_options": "Opțiuni de descărcare",
"download_path": "Locația de descărcare",
"previous_screenshot": "Captura de ecran anterioară",
"next_screenshot": "Captura de ecran următoare",
"screenshot": "Captură de ecran {{number}}",
"open_screenshot": "Deschide captura de ecran {{number}}",
"download_settings": "Setări de descărcare",
"downloader": "Program de descărcare"
},
"activation": {
"title": "Activează Hydra",
"installation_id": "ID-ul de instalare:",
"enter_activation_code": "Introdu codul de activare",
"message": "Dacă nu știi de unde să ceri acest lucru, atunci nu ar trebui să-l ai.",
"activate": "Activează",
"loading": "Se încarcă..."
},
"downloads": {
"resume": "Reia",
"pause": "Pauză",
"eta": "Concluzie {{eta}}",
"paused": "Pauzat",
"verifying": "Se verifică...",
"completed": "Completat",
"removed": "Nu este descărcat",
"cancel": "Anulează",
"filter": "Filtrează jocurile descărcate",
"remove": "Elimină",
"downloading_metadata": "Se descarcă metadata...",
"deleting": "Se șterge programul de instalare...",
"delete": "Elimină programul de instalare",
"delete_modal_title": "Ești sigur?",
"delete_modal_description": "Aceasta va elimina toate fișierele de instalare de pe computer",
"install": "Instalează"
},
"settings": {
"downloads_path": "Locația de descărcare",
"change": "Actualizează",
"notifications": "Notificări",
"enable_download_notifications": "Când o descărcare este completă",
"enable_repack_list_notifications": "Când un nou repack este adăugat",
"real_debrid_api_token_label": "Token API Real-Debrid",
"quit_app_instead_hiding": "Nu ascunde Hydra la închidere",
"launch_with_system": "Lansează Hydra la pornirea sistemului",
"general": "General",
"behavior": "Comportament",
"language": "Limbă",
"real_debrid_api_token": "Token API",
"enable_real_debrid": "Activează Real-Debrid",
"real_debrid_description": "Real-Debrid este un descărcător fără restricții care îți permite să descarci fișiere instantaneu și la cea mai bună viteză a internetului tău.",
"real_debrid_invalid_token": "Token API invalid",
"real_debrid_api_token_hint": "Poți obține token-ul tău API <0>aici</0>",
"real_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la Real-Debrid",
"real_debrid_linked_message": "Contul \"{{username}}\" a fost legat",
"save_changes": "Salvează modificările",
"changes_saved": "Modificările au fost salvate cu succes"
},
"notifications": {
"download_complete": "Descărcare completă",
"game_ready_to_install": "{{title}} este gata de instalare",
"repack_list_updated": "Lista de repack-uri a fost actualizată",
"repack_count_one": "{{count}} repack adăugat",
"repack_count_other": "{{count}} repack-uri adăugate"
},
"system_tray": {
"open": "Deschide Hydra",
"quit": "Ieși"
},
"game_card": {
"no_downloads": "Nicio descărcare disponibilă"
},
"binary_not_found_modal": {
"title": "Programele nu sunt instalate",
"description": "Fișierele executabile Wine sau Lutris nu au fost găsite pe sistemul tău",
"instructions": "Verifică modul corect de instalare a oricăruia dintre acestea pe distribuția ta Linux pentru ca jocul să ruleze normal"
},
"modal": {
"close": "Buton de închidere"
}
}

View File

@@ -153,11 +153,11 @@
"enable_download_notifications": "По завершении загрузки", "enable_download_notifications": "По завершении загрузки",
"enable_repack_list_notifications": "При добавлении нового репака", "enable_repack_list_notifications": "При добавлении нового репака",
"real_debrid_api_token_label": "Real-Debrid API-токен", "real_debrid_api_token_label": "Real-Debrid API-токен",
"quit_app_instead_hiding": "Закрывать Hydra вместо того, чтобы сворачивать его в трей", "quit_app_instead_hiding": "Закрывать приложение вместо сворачивания в трей",
"launch_with_system": "Запуск Hydra вместе с системой", "launch_with_system": "Запускать Hydra вместе с системой",
"general": "Основные", "general": "Основные",
"behavior": "Поведение", "behavior": "Поведение",
"download_sources": "Скачать исходный код", "download_sources": "Источники загрузки",
"language": "Язык", "language": "Язык",
"real_debrid_api_token": "API Ключ", "real_debrid_api_token": "API Ключ",
"enable_real_debrid": "Включить Real-Debrid", "enable_real_debrid": "Включить Real-Debrid",

View File

@@ -1,4 +1,7 @@
{ {
"app": {
"successfully_signed_in": "已成功登录"
},
"home": { "home": {
"featured": "特色推荐", "featured": "特色推荐",
"trending": "最近热门", "trending": "最近热门",
@@ -14,20 +17,26 @@
"paused": "{{title}} (已暂停)", "paused": "{{title}} (已暂停)",
"downloading": "{{title}} ({{percentage}} - 正在下载…)", "downloading": "{{title}} ({{percentage}} - 正在下载…)",
"filter": "筛选游戏库", "filter": "筛选游戏库",
"home": "主页" "home": "主页",
"queued": "{{title}} (已加入下载队列)",
"game_has_no_executable": "未选择游戏的可执行文件",
"sign_in": "登入"
}, },
"header": { "header": {
"search": "搜索", "search": "搜索游戏",
"home": "主页", "home": "主页",
"catalogue": "游戏目录", "catalogue": "游戏目录",
"downloads": "下载中心", "downloads": "下载中心",
"search_results": "搜索结果", "search_results": "搜索结果",
"settings": "设置" "settings": "设置",
"version_available_install": "版本 {{version}} 已可用. 点击此处重新启动并安装.",
"version_available_download": "版本 {{version}} 可用. 点击此处下载."
}, },
"bottom_panel": { "bottom_panel": {
"no_downloads_in_progress": "没有正在进行的下载", "no_downloads_in_progress": "没有正在进行的下载",
"downloading_metadata": "正在下载{{title}}的元数据…", "downloading_metadata": "正在下载{{title}}的元数据…",
"downloading": "正在下载{{title}}… ({{percentage}}完成) - 剩余时间{{eta}} - 速度{{speed}}" "downloading": "正在下载{{title}}… ({{percentage}}完成) - 剩余时间{{eta}} - 速度{{speed}}",
"calculating_eta": "正在下载 {{title}}… (已完成{{percentage}}.) - 正在计算剩余时间..."
}, },
"catalogue": { "catalogue": {
"next_page": "下一页", "next_page": "下一页",
@@ -76,7 +85,29 @@
"previous_screenshot": "上一张截图", "previous_screenshot": "上一张截图",
"next_screenshot": "下一张截图", "next_screenshot": "下一张截图",
"screenshot": "截图 {{number}}", "screenshot": "截图 {{number}}",
"open_screenshot": "打开截图 {{number}}" "open_screenshot": "打开截图 {{number}}",
"download_settings": "下载设置",
"downloader": "下载器",
"select_executable": "选择",
"no_executable_selected": "没有可执行文件被指定",
"open_folder": "打开目录",
"open_download_location": "查看已下载的文件",
"create_shortcut": "创建桌面快捷方式",
"remove_files": "删除文件",
"remove_from_library_title": "你确定吗?",
"remove_from_library_description": "这将会把 {{game}} 从你的库中移除",
"options": "选项",
"executable_section_title": "可执行文件",
"executable_section_description": "点击 \"Play\" 时将执行的文件的路径",
"downloads_secion_title": "下载",
"downloads_section_description": "查看此游戏的更新或其他版本",
"danger_zone_section_title": "危险操作",
"danger_zone_section_description": "从您的库或Hydra下载的文件中删除此游戏",
"download_in_progress": "下载进行中",
"download_paused": "下载暂停",
"last_downloaded_option": "上次下载的选项",
"create_shortcut_success": "成功创建快捷方式",
"create_shortcut_error": "创建快捷方式出错"
}, },
"activation": { "activation": {
"title": "激活 Hydra", "title": "激活 Hydra",
@@ -101,7 +132,13 @@
"delete": "移除安装程序", "delete": "移除安装程序",
"delete_modal_title": "您确定吗?", "delete_modal_title": "您确定吗?",
"delete_modal_description": "这将从您的电脑上移除所有的安装文件", "delete_modal_description": "这将从您的电脑上移除所有的安装文件",
"install": "安装" "install": "安装",
"download_in_progress": "进行中",
"queued_downloads": "在队列中的下载",
"downloads_completed": "已完成",
"queued": "下载列表",
"no_downloads_title": "空空如也",
"no_downloads_description": "你还未使用Hydra下载任何游戏,但什么时候开始,都为时不晚。"
}, },
"settings": { "settings": {
"downloads_path": "下载路径", "downloads_path": "下载路径",
@@ -109,34 +146,72 @@
"notifications": "通知", "notifications": "通知",
"enable_download_notifications": "下载完成时", "enable_download_notifications": "下载完成时",
"enable_repack_list_notifications": "添加新重打包时", "enable_repack_list_notifications": "添加新重打包时",
"real_debrid_api_token_label": "Real-Debrid API 令牌",
"quit_app_instead_hiding": "关闭Hydra而不是最小化到托盘",
"launch_with_system": "系统启动时运行 Hydra",
"general": "通用",
"behavior": "行为", "behavior": "行为",
"general": "常规", "download_sources": "下载源",
"quit_app_instead_hiding": "关闭应用程序而不是最小化到托盘", "language": "语言",
"launch_with_system": "随系统启动时运行应用程序", "real_debrid_api_token": "API 令牌",
"enable_real_debrid": "启用 Real-Debrid", "enable_real_debrid": "启用 Real-Debrid",
"real_debrid_description": "Real-Debrid 是一个无限制的下载器,允许您以最快的互联网速度即时下载文件。",
"real_debrid_invalid_token": "无效的 API 令牌",
"real_debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.", "real_debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.",
"save_changes": "保存更改" "real_debrid_free_account_error": "账户 \"{{username}}\" 是免费账户。请订阅 Real-Debrid",
}, "real_debrid_linked_message": "账户 \"{{username}}\" 已链接",
"notifications": { "save_changes": "保存更改",
"download_complete": "下载完成", "changes_saved": "更改已成功保存",
"game_ready_to_install": "{{title}}已准备好安装", "download_sources_description": "Hydra 将从这些源获取下载链接。源 URL 必须是直接链接到包含下载链接的 .json 文件。",
"repack_list_updated": "重打包列表已更新", "validate_download_source": "验证",
"repack_count_one": "已添加{{count}}个重打包", "remove_download_source": "移除",
"repack_count_other": "添加{{count}}个重打包" "add_download_source": "添加源",
}, "download_count_zero": "列表中无下载",
"system_tray": { "download_count_one": "列表中有 {{countFormatted}} 个下载",
"open": "打开Hydra", "download_count_other": "列表中有 {{countFormatted}} 个下载",
"quit": "退出" "download_options_zero": "无可用下载",
}, "download_options_one": "有 {{countFormatted}} 个下载可用",
"game_card": { "download_options_other": "有 {{countFormatted}} 个下载可用",
"no_downloads": "没有可用的下载" "download_source_url": "下载源 URL",
}, "add_download_source_description": "插入包含 .json 文件的 URL",
"binary_not_found_modal": { "download_source_up_to_date": "已更新",
"title": "程序未安装", "download_source_errored": "出错",
"description": "在您的系统上未找到Wine或Lutris的可执行文件", "sync_download_sources": "同步源",
"instructions": "检查在您的Linux发行版上正确安装它们的方法,以便游戏可以正常运行" "removed_download_source": "已移除下载源",
"added_download_source": "已添加下载源",
"download_sources_synced": "所有下载源已同步",
"insert_valid_json_url": "插入有效的 JSON 网址",
"found_download_option_zero": "未找到下载选项",
"found_download_option_one": "找到 {{countFormatted}} 个下载选项",
"found_download_option_other": "找到 {{countFormatted}} 个下载选项",
"import": "导入"
}, },
"modal": { "modal": {
"close": "关闭按钮" "close": "关闭按钮"
},
"forms": {
"toggle_password_visibility": "切换密码可见性"
},
"user_profile": {
"amount_hours": "{{amount}} 小时",
"amount_minutes": "{{amount}} 分钟",
"last_time_played": "上次游玩时间 {{period}}",
"activity": "近期活动",
"library": "库",
"total_play_time": "总游戏时长: {{amount}}",
"no_recent_activity_title": "Emmm… 这里暂时啥都没有",
"no_recent_activity_description": "你最近没玩过任何游戏。是时候做出改变了!",
"display_name": "昵称",
"saving": "保存中",
"save": "保存",
"edit_profile": "编辑资料",
"saved_successfully": "成功保存",
"try_again": "请重试",
"sign_out_modal_title": "你确定吗?",
"cancel": "取消",
"successfully_signed_out": "登出成功",
"sign_out": "登出",
"playing_for": "Playing for {{amount}}",
"sign_out_modal_text": "您的资料库与您当前的账户相关联。注销后,您的资料库将不再可见,任何进度也不会保存。继续退出吗?"
} }
} }

View File

@@ -14,3 +14,12 @@ export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");
export const seedsPath = app.isPackaged export const seedsPath = app.isPackaged
? path.join(process.resourcesPath, "seeds") ? path.join(process.resourcesPath, "seeds")
: path.join(__dirname, "..", "..", "seeds"); : path.join(__dirname, "..", "..", "seeds");
export const windowsStartupPath = path.join(
app.getPath("appData"),
"Microsoft",
"Windows",
"Start Menu",
"Programs",
"Startup"
);

View File

@@ -1,80 +0,0 @@
declare module "aria2" {
export type Aria2Status =
| "active"
| "waiting"
| "paused"
| "error"
| "complete"
| "removed";
export interface StatusResponse {
gid: string;
status: Aria2Status;
totalLength: string;
completedLength: string;
uploadLength: string;
bitfield: string;
downloadSpeed: string;
uploadSpeed: string;
infoHash?: string;
numSeeders?: string;
seeder?: boolean;
pieceLength: string;
numPieces: string;
connections: string;
errorCode?: string;
errorMessage?: string;
followedBy?: string[];
following: string;
belongsTo: string;
dir: string;
files: {
path: string;
length: string;
completedLength: string;
selected: string;
}[];
bittorrent?: {
announceList: string[][];
comment: string;
creationDate: string;
mode: "single" | "multi";
info: {
name: string;
verifiedLength: string;
verifyIntegrityPending: string;
};
};
}
export default class Aria2 {
constructor(options: any);
open: () => Promise<void>;
call(
method: "addUri",
uris: string[],
options: { dir: string }
): Promise<string>;
call(
method: "tellStatus",
gid: string,
keys?: string[]
): Promise<StatusResponse>;
call(method: "pause", gid: string): Promise<string>;
call(method: "forcePause", gid: string): Promise<string>;
call(method: "unpause", gid: string): Promise<string>;
call(method: "remove", gid: string): Promise<string>;
call(method: "forceRemove", gid: string): Promise<string>;
call(method: "pauseAll"): Promise<string>;
call(method: "forcePauseAll"): Promise<string>;
listNotifications: () => [
"onDownloadStart",
"onDownloadPause",
"onDownloadStop",
"onDownloadComplete",
"onDownloadError",
"onBtDownloadComplete",
];
on: (event: string, callback: (params: any) => void) => void;
}
}

View File

@@ -9,9 +9,8 @@ import {
} from "typeorm"; } from "typeorm";
import { Repack } from "./repack.entity"; import { Repack } from "./repack.entity";
import type { GameShop } from "@types"; import type { GameShop, GameStatus } from "@types";
import { Downloader } from "@shared"; import { Downloader } from "@shared";
import type { Aria2Status } from "aria2";
import type { DownloadQueue } from "./download-queue.entity"; import type { DownloadQueue } from "./download-queue.entity";
@Entity("game") @Entity("game")
@@ -47,7 +46,7 @@ export class Game {
shop: GameShop; shop: GameShop;
@Column("text", { nullable: true }) @Column("text", { nullable: true })
status: Aria2Status | null; status: GameStatus | null;
@Column("int", { default: Downloader.Torrent }) @Column("int", { default: Downloader.Torrent })
downloader: Downloader; downloader: Downloader;

View File

@@ -1,4 +1,5 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import * as Sentry from "@sentry/electron/main";
import { userAuthRepository } from "@main/repository"; import { userAuthRepository } from "@main/repository";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
@@ -8,6 +9,9 @@ const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
if (!auth) return null; if (!auth) return null;
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload; const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
Sentry.setContext("sessionId", payload.sessionId);
return payload.sessionId; return payload.sessionId;
}; };

View File

@@ -1,5 +1,6 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services"; import * as Sentry from "@sentry/electron/main";
import { HydraApi, TorrentDownloader, gamesPlaytime } from "@main/services";
import { dataSource } from "@main/data-source"; import { dataSource } from "@main/data-source";
import { DownloadQueue, Game, UserAuth } from "@main/entity"; import { DownloadQueue, Game, UserAuth } from "@main/entity";
@@ -19,8 +20,11 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
gamesPlaytime.clear(); gamesPlaytime.clear();
}); });
/* Disconnects aria2 */ /* Removes user from Sentry */
DownloadManager.disconnect(); Sentry.setUser(null);
/* Disconnects libtorrent */
TorrentDownloader.kill();
await Promise.all([ await Promise.all([
databaseOperations, databaseOperations,

View File

@@ -0,0 +1,10 @@
import { shell } from "electron";
export const parseExecutablePath = (path: string) => {
if (process.platform === "win32" && path.endsWith(".lnk")) {
const { target } = shell.readShortcutLink(path);
return target;
}
return path;
};

View File

@@ -49,4 +49,8 @@ import "./profile/update-profile";
ipcMain.handle("ping", () => "pong"); ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => app.getVersion()); ipcMain.handle("getVersion", () => app.getVersion());
ipcMain.handle(
"isPortableVersion",
() => process.env.PORTABLE_EXECUTABLE_FILE != null
);
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath); ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);

View File

@@ -45,10 +45,6 @@ const deleteGameFolder = async (
reject(); reject();
} }
const aria2ControlFilePath = `${folderPath}.aria2`;
if (fs.existsSync(aria2ControlFilePath))
fs.rmSync(aria2ControlFilePath);
resolve(); resolve();
} }
); );

View File

@@ -2,15 +2,18 @@ import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { shell } from "electron"; import { shell } from "electron";
import { parseExecutablePath } from "../helpers/parse-executable-path";
const openGame = async ( const openGame = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
gameId: number, gameId: number,
executablePath: string executablePath: string
) => { ) => {
await gameRepository.update({ id: gameId }, { executablePath }); const parsedPath = parseExecutablePath(executablePath);
shell.openPath(executablePath); await gameRepository.update({ id: gameId }, { executablePath: parsedPath });
shell.openPath(parsedPath);
}; };
registerEvent("openGame", openGame); registerEvent("openGame", openGame);

View File

@@ -1,6 +1,7 @@
import { gameRepository } from "@main/repository"; import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { parseExecutablePath } from "../helpers/parse-executable-path";
const updateExecutablePath = async ( const updateExecutablePath = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@@ -12,7 +13,7 @@ const updateExecutablePath = async (
id, id,
}, },
{ {
executablePath, executablePath: parseExecutablePath(executablePath),
} }
); );
}; };

View File

@@ -1,4 +1,5 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import * as Sentry from "@sentry/electron/main";
import { HydraApi } from "@main/services"; import { HydraApi } from "@main/services";
import { UserProfile } from "@types"; import { UserProfile } from "@types";
import { userAuthRepository } from "@main/repository"; import { userAuthRepository } from "@main/repository";
@@ -21,10 +22,12 @@ const getMe = async (
["id"] ["id"]
); );
Sentry.setUser({ id: me.id, username: me.username });
return me; return me;
}) })
.catch((err) => { .catch((err) => {
logger.error("getMe", err); logger.error("getMe", err.message);
return userAuthRepository.findOne({ where: { id: 1 } }); return userAuthRepository.findOne({ where: { id: 1 } });
}); });
}; };

View File

@@ -1,18 +1,37 @@
import { windowsStartupPath } from "@main/constants";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import AutoLaunch from "auto-launch"; import AutoLaunch from "auto-launch";
import { app } from "electron"; import { app } from "electron";
import fs from "node:fs";
import path from "node:path";
const autoLaunch = async ( const autoLaunch = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
enabled: boolean enabled: boolean
) => { ) => {
if (!app.isPackaged) return;
const appLauncher = new AutoLaunch({ const appLauncher = new AutoLaunch({
name: app.getName(), name: app.getName(),
}); });
if (enabled) {
appLauncher.enable().catch(); if (process.platform == "win32") {
const destination = path.join(windowsStartupPath, "Hydra.vbs");
if (enabled) {
const scriptPath = path.join(process.resourcesPath, "hydralauncher.vbs");
fs.copyFileSync(scriptPath, destination);
} else {
appLauncher.disable().catch();
fs.rmSync(destination);
}
} else { } else {
appLauncher.disable().catch(); if (enabled) {
appLauncher.enable().catch();
} else {
appLauncher.disable().catch();
}
} }
}; };

View File

@@ -11,7 +11,15 @@ export const getProcesses = async () => {
if (process.platform == "win32") { if (process.platform == "win32") {
const binaryPath = app.isPackaged const binaryPath = app.isPackaged
? path.join(process.resourcesPath, "fastlist.exe") ? path.join(process.resourcesPath, "fastlist.exe")
: path.join(__dirname, "..", "..", "fastlist.exe"); : path.join(
__dirname,
"..",
"..",
"node_modules",
"ps-list",
"vendor",
"fastlist-0.3.0-x64.exe"
);
const { stdout } = await execFile(binaryPath, { const { stdout } = await execFile(binaryPath, {
maxBuffer: TEN_MEGABYTES, maxBuffer: TEN_MEGABYTES,

View File

@@ -1,10 +1,11 @@
import { app, BrowserWindow, net, protocol } from "electron"; import { app, BrowserWindow, net, protocol } from "electron";
import { init } from "@sentry/electron/main";
import updater from "electron-updater"; import updater from "electron-updater";
import i18n from "i18next"; import i18n from "i18next";
import path from "node:path"; import path from "node:path";
import url from "node:url"; import url from "node:url";
import { electronApp, optimizer } from "@electron-toolkit/utils"; import { electronApp, optimizer } from "@electron-toolkit/utils";
import { DownloadManager, logger, WindowManager } from "@main/services"; import { logger, TorrentDownloader, WindowManager } from "@main/services";
import { dataSource } from "@main/data-source"; import { dataSource } from "@main/data-source";
import * as resources from "@locales"; import * as resources from "@locales";
import { userPreferencesRepository } from "@main/repository"; import { userPreferencesRepository } from "@main/repository";
@@ -22,6 +23,12 @@ autoUpdater.logger = logger;
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit(); if (!gotTheLock) app.quit();
if (import.meta.env.MAIN_VITE_SENTRY_DSN) {
init({
dsn: import.meta.env.MAIN_VITE_SENTRY_DSN,
});
}
app.commandLine.appendSwitch("--no-sandbox"); app.commandLine.appendSwitch("--no-sandbox");
i18n.init({ i18n.init({
@@ -108,7 +115,8 @@ app.on("window-all-closed", () => {
}); });
app.on("before-quit", () => { app.on("before-quit", () => {
DownloadManager.disconnect(); /* Disconnects libtorrent */
TorrentDownloader.kill();
}); });
app.on("activate", () => { app.on("activate", () => {

View File

@@ -1,20 +0,0 @@
import path from "node:path";
import { spawn } from "node:child_process";
import { app } from "electron";
export const startAria2 = () => {
const binaryPath = app.isPackaged
? path.join(process.resourcesPath, "aria2", "aria2c")
: path.join(__dirname, "..", "..", "aria2", "aria2c");
return spawn(
binaryPath,
[
"--enable-rpc",
"--rpc-listen-all",
"--file-allocation=none",
"--allow-overwrite=true",
],
{ stdio: "inherit", windowsHide: true }
);
};

View File

@@ -1,304 +0,0 @@
import Aria2, { StatusResponse } from "aria2";
import path from "node:path";
import { downloadQueueRepository, gameRepository } from "@main/repository";
import { WindowManager } from "./window-manager";
import { RealDebridClient } from "./real-debrid";
import { Downloader } from "@shared";
import { DownloadProgress } from "@types";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { Game } from "@main/entity";
import { startAria2 } from "./aria2c";
import { sleep } from "@main/helpers";
import { logger } from "./logger";
import type { ChildProcess } from "node:child_process";
import { publishDownloadCompleteNotification } from "./notifications";
export class DownloadManager {
private static downloads = new Map<number, string>();
private static connected = false;
private static gid: string | null = null;
private static game: Game | null = null;
private static realDebridTorrentId: string | null = null;
private static aria2c: ChildProcess | null = null;
private static aria2 = new Aria2({});
private static async connect() {
this.aria2c = startAria2();
let retries = 0;
while (retries < 4 && !this.connected) {
try {
await this.aria2.open();
logger.log("Connected to aria2");
this.connected = true;
} catch (err) {
await sleep(100);
logger.log("Failed to connect to aria2, retrying...");
retries++;
}
}
}
public static disconnect() {
if (this.aria2c) {
this.aria2c.kill();
this.connected = false;
}
}
private static getETA(
totalLength: number,
completedLength: number,
speed: number
) {
const remainingBytes = totalLength - completedLength;
if (remainingBytes >= 0 && speed > 0) {
return (remainingBytes / speed) * 1000;
}
return -1;
}
private static getFolderName(status: StatusResponse) {
if (status.bittorrent?.info) return status.bittorrent.info.name;
const [file] = status.files;
if (file) return path.win32.basename(file.path);
return null;
}
private static async getRealDebridDownloadUrl() {
if (this.realDebridTorrentId) {
const torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId
);
const { status, links } = torrentInfo;
if (status === "waiting_files_selection") {
await RealDebridClient.selectAllFiles(this.realDebridTorrentId);
return null;
}
if (status === "downloaded") {
const [link] = links;
const { download } = await RealDebridClient.unrestrictLink(link);
return decodeURIComponent(download);
}
if (WindowManager.mainWindow) {
const progress = torrentInfo.progress / 100;
const totalDownloaded = progress * torrentInfo.bytes;
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
const payload = {
numPeers: 0,
numSeeds: torrentInfo.seeders,
downloadSpeed: torrentInfo.speed,
timeRemaining: this.getETA(
torrentInfo.bytes,
totalDownloaded,
torrentInfo.speed
),
isDownloadingMetadata: status === "magnet_conversion",
game: {
...this.game,
bytesDownloaded: progress * torrentInfo.bytes,
progress,
},
} as DownloadProgress;
WindowManager.mainWindow.webContents.send(
"on-download-progress",
JSON.parse(JSON.stringify(payload))
);
}
}
return null;
}
public static async watchDownloads() {
if (!this.game) return;
if (!this.gid && this.realDebridTorrentId) {
const options = { dir: this.game.downloadPath! };
const downloadUrl = await this.getRealDebridDownloadUrl();
if (downloadUrl) {
this.gid = await this.aria2.call("addUri", [downloadUrl], options);
this.downloads.set(this.game.id, this.gid);
this.realDebridTorrentId = null;
}
}
if (!this.gid) return;
const status = await this.aria2.call("tellStatus", this.gid);
const isDownloadingMetadata = status.bittorrent && !status.bittorrent?.info;
if (status.followedBy?.length) {
this.gid = status.followedBy[0];
this.downloads.set(this.game.id, this.gid);
return;
}
const progress =
Number(status.completedLength) / Number(status.totalLength);
if (!isDownloadingMetadata) {
const update: QueryDeepPartialEntity<Game> = {
bytesDownloaded: Number(status.completedLength),
fileSize: Number(status.totalLength),
status: status.status,
};
if (!isNaN(progress)) update.progress = progress;
await gameRepository.update(
{ id: this.game.id },
{
...update,
status: status.status,
folderName: this.getFolderName(status),
}
);
}
const game = await gameRepository.findOne({
where: { id: this.game.id, isDeleted: false },
});
if (WindowManager.mainWindow && game) {
if (!isNaN(progress))
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
const payload = {
numPeers: Number(status.connections),
numSeeds: Number(status.numSeeders ?? 0),
downloadSpeed: Number(status.downloadSpeed),
timeRemaining: this.getETA(
Number(status.totalLength),
Number(status.completedLength),
Number(status.downloadSpeed)
),
isDownloadingMetadata: !!isDownloadingMetadata,
game,
} as DownloadProgress;
WindowManager.mainWindow.webContents.send(
"on-download-progress",
JSON.parse(JSON.stringify(payload))
);
}
if (progress === 1 && this.game && !isDownloadingMetadata) {
publishDownloadCompleteNotification(this.game);
await downloadQueueRepository.delete({ game: this.game });
/*
Only cancel bittorrent downloads to stop seeding
*/
if (status.bittorrent) {
await this.cancelDownload(this.game.id);
} else {
this.clearCurrentDownload();
}
const [nextQueueItem] = await downloadQueueRepository.find({
order: {
id: "DESC",
},
relations: {
game: true,
},
});
if (nextQueueItem) {
this.resumeDownload(nextQueueItem.game);
}
}
}
private static clearCurrentDownload() {
if (this.game) {
this.downloads.delete(this.game.id);
this.gid = null;
this.game = null;
this.realDebridTorrentId = null;
}
}
static async cancelDownload(gameId: number) {
const gid = this.downloads.get(gameId);
if (gid) {
await this.aria2.call("forceRemove", gid);
if (this.gid === gid) {
this.clearCurrentDownload();
WindowManager.mainWindow?.setProgressBar(-1);
} else {
this.downloads.delete(gameId);
}
}
}
static async pauseDownload() {
if (this.gid) {
await this.aria2.call("forcePause", this.gid);
this.gid = null;
}
this.game = null;
this.realDebridTorrentId = null;
WindowManager.mainWindow?.setProgressBar(-1);
}
static async resumeDownload(game: Game) {
if (this.downloads.has(game.id)) {
const gid = this.downloads.get(game.id)!;
await this.aria2.call("unpause", gid);
this.gid = gid;
this.game = game;
this.realDebridTorrentId = null;
} else {
return this.startDownload(game);
}
}
static async startDownload(game: Game) {
if (!this.connected) await this.connect();
const options = {
dir: game.downloadPath!,
};
if (game.downloader === Downloader.RealDebrid) {
this.realDebridTorrentId = await RealDebridClient.getTorrentId(
game!.uri!
);
} else {
this.gid = await this.aria2.call("addUri", [game.uri!], options);
this.downloads.set(game.id, this.gid);
}
this.game = game;
}
}

View File

@@ -0,0 +1,105 @@
import { Game } from "@main/entity";
import { Downloader } from "@shared";
import { TorrentDownloader } from "./torrent-downloader";
import { WindowManager } from "../window-manager";
import { downloadQueueRepository, gameRepository } from "@main/repository";
import { publishDownloadCompleteNotification } from "../notifications";
import { RealDebridDownloader } from "./real-debrid-downloader";
import type { DownloadProgress } from "@types";
export class DownloadManager {
private static currentDownloader: Downloader | null = null;
public static async watchDownloads() {
let status: DownloadProgress | null = null;
if (this.currentDownloader === Downloader.RealDebrid) {
status = await RealDebridDownloader.getStatus();
} else {
status = await TorrentDownloader.getStatus();
}
if (status) {
const { gameId, progress } = status;
const game = await gameRepository.findOne({
where: { id: gameId, isDeleted: false },
});
if (WindowManager.mainWindow && game) {
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
WindowManager.mainWindow.webContents.send(
"on-download-progress",
JSON.parse(
JSON.stringify({
...status,
game,
})
)
);
}
if (progress === 1 && game) {
publishDownloadCompleteNotification(game);
await downloadQueueRepository.delete({ game });
const [nextQueueItem] = await downloadQueueRepository.find({
order: {
id: "DESC",
},
relations: {
game: true,
},
});
if (nextQueueItem) {
this.resumeDownload(nextQueueItem.game);
}
}
}
}
static async pauseDownload() {
if (this.currentDownloader === Downloader.RealDebrid) {
RealDebridDownloader.pauseDownload();
} else {
await TorrentDownloader.pauseDownload();
}
WindowManager.mainWindow?.setProgressBar(-1);
this.currentDownloader = null;
}
static async resumeDownload(game: Game) {
if (game.downloader === Downloader.RealDebrid) {
RealDebridDownloader.startDownload(game);
this.currentDownloader = Downloader.RealDebrid;
} else {
TorrentDownloader.startDownload(game);
this.currentDownloader = Downloader.Torrent;
}
}
static async cancelDownload(gameId: number) {
if (this.currentDownloader === Downloader.RealDebrid) {
RealDebridDownloader.cancelDownload();
} else {
TorrentDownloader.cancelDownload(gameId);
}
WindowManager.mainWindow?.setProgressBar(-1);
this.currentDownloader = null;
}
static async startDownload(game: Game) {
if (game.downloader === Downloader.RealDebrid) {
RealDebridDownloader.startDownload(game);
this.currentDownloader = Downloader.RealDebrid;
} else {
TorrentDownloader.startDownload(game);
this.currentDownloader = Downloader.Torrent;
}
}
}

View File

@@ -0,0 +1,13 @@
export const calculateETA = (
totalLength: number,
completedLength: number,
speed: number
) => {
const remainingBytes = totalLength - completedLength;
if (remainingBytes >= 0 && speed > 0) {
return (remainingBytes / speed) * 1000;
}
return -1;
};

View File

@@ -0,0 +1,123 @@
import path from "node:path";
import fs from "node:fs";
import crypto from "node:crypto";
import axios, { type AxiosProgressEvent } from "axios";
import { app } from "electron";
import { logger } from "../logger";
export class HttpDownload {
private abortController: AbortController;
public lastProgressEvent: AxiosProgressEvent;
private trackerFilePath: string;
private trackerProgressEvent: AxiosProgressEvent | null = null;
private downloadPath: string;
private downloadTrackersPath = path.join(
app.getPath("documents"),
"Hydra",
"Downloads"
);
constructor(
private url: string,
private savePath: string
) {
this.abortController = new AbortController();
const sha256Hasher = crypto.createHash("sha256");
const hash = sha256Hasher.update(url).digest("hex");
this.trackerFilePath = path.join(
this.downloadTrackersPath,
`${hash}.hydradownload`
);
const filename = path.win32.basename(this.url);
this.downloadPath = path.join(this.savePath, filename);
}
private updateTrackerFile() {
if (!fs.existsSync(this.downloadTrackersPath)) {
fs.mkdirSync(this.downloadTrackersPath, {
recursive: true,
});
}
fs.writeFileSync(
this.trackerFilePath,
JSON.stringify(this.lastProgressEvent),
{ encoding: "utf-8" }
);
}
private removeTrackerFile() {
if (fs.existsSync(this.trackerFilePath)) {
fs.rm(this.trackerFilePath, (err) => {
logger.error(err);
});
}
}
public async startDownload() {
// Check if there's already a tracker file and download file
if (
fs.existsSync(this.trackerFilePath) &&
fs.existsSync(this.downloadPath)
) {
this.trackerProgressEvent = JSON.parse(
fs.readFileSync(this.trackerFilePath, { encoding: "utf-8" })
);
}
const response = await axios.get(this.url, {
responseType: "stream",
signal: this.abortController.signal,
headers: {
Range: `bytes=${this.trackerProgressEvent?.loaded ?? 0}-`,
},
onDownloadProgress: (progressEvent) => {
const total =
this.trackerProgressEvent?.total ?? progressEvent.total ?? 0;
const loaded =
(this.trackerProgressEvent?.loaded ?? 0) + progressEvent.loaded;
const progress = loaded / total;
this.lastProgressEvent = {
...progressEvent,
total,
progress,
loaded,
};
this.updateTrackerFile();
if (progressEvent.progress === 1) {
this.removeTrackerFile();
}
},
});
response.data.pipe(
fs.createWriteStream(this.downloadPath, {
flags: "a",
})
);
}
public async pauseDownload() {
this.abortController.abort();
}
public cancelDownload() {
this.pauseDownload();
fs.rm(this.downloadPath, (err) => {
if (err) logger.error(err);
});
fs.rm(this.trackerFilePath, (err) => {
if (err) logger.error(err);
});
}
}

View File

@@ -0,0 +1,2 @@
export * from "./download-manager";
export * from "./torrent-downloader";

View File

@@ -0,0 +1,125 @@
import { Game } from "@main/entity";
import { RealDebridClient } from "../real-debrid";
import { gameRepository } from "@main/repository";
import { calculateETA } from "./helpers";
import { DownloadProgress } from "@types";
import { HttpDownload } from "./http-download";
export class RealDebridDownloader {
private static downloadingGame: Game | null = null;
private static realDebridTorrentId: string | null = null;
private static httpDownload: HttpDownload | null = null;
private static async getRealDebridDownloadUrl() {
if (this.realDebridTorrentId) {
const torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId
);
const { status, links } = torrentInfo;
if (status === "waiting_files_selection") {
await RealDebridClient.selectAllFiles(this.realDebridTorrentId);
return null;
}
if (status === "downloaded") {
const [link] = links;
const { download } = await RealDebridClient.unrestrictLink(link);
return decodeURIComponent(download);
}
}
return null;
}
public static async getStatus() {
const lastProgressEvent = this.httpDownload?.lastProgressEvent;
if (lastProgressEvent) {
await gameRepository.update(
{ id: this.downloadingGame!.id },
{
bytesDownloaded: lastProgressEvent.loaded,
fileSize: lastProgressEvent.total,
progress: lastProgressEvent.progress,
status: "active",
}
);
const progress = {
numPeers: 0,
numSeeds: 0,
downloadSpeed: lastProgressEvent.rate,
timeRemaining: calculateETA(
lastProgressEvent.total ?? 0,
lastProgressEvent.loaded,
lastProgressEvent.rate ?? 0
),
isDownloadingMetadata: false,
isCheckingFiles: false,
progress: lastProgressEvent.progress,
gameId: this.downloadingGame!.id,
} as DownloadProgress;
if (lastProgressEvent.progress === 1) {
this.pauseDownload();
}
return progress;
}
if (this.realDebridTorrentId && this.downloadingGame) {
const torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId
);
const { status } = torrentInfo;
if (status === "downloaded") {
this.startDownload(this.downloadingGame);
}
const progress = torrentInfo.progress / 100;
const totalDownloaded = progress * torrentInfo.bytes;
return {
numPeers: 0,
numSeeds: torrentInfo.seeders,
downloadSpeed: torrentInfo.speed,
timeRemaining: calculateETA(
torrentInfo.bytes,
totalDownloaded,
torrentInfo.speed
),
isDownloadingMetadata: status === "magnet_conversion",
} as DownloadProgress;
}
return null;
}
static async pauseDownload() {
this.httpDownload?.pauseDownload();
this.realDebridTorrentId = null;
this.downloadingGame = null;
}
static async startDownload(game: Game) {
this.realDebridTorrentId = await RealDebridClient.getTorrentId(game!.uri!);
this.downloadingGame = game;
const downloadUrl = await this.getRealDebridDownloadUrl();
if (downloadUrl) {
this.realDebridTorrentId = null;
this.httpDownload = new HttpDownload(downloadUrl, game!.downloadPath!);
this.httpDownload.startDownload();
}
}
static cancelDownload() {
return this.httpDownload?.cancelDownload();
}
}

View File

@@ -0,0 +1,60 @@
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";
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");
export const startTorrentClient = (args: StartDownloadPayload) => {
const commonArgs = [
BITTORRENT_PORT,
RPC_PORT,
RPC_PASSWORD,
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();
}
return cp.spawn(binaryPath, commonArgs, {
stdio: "inherit",
windowsHide: true,
});
} else {
const scriptPath = path.join(
__dirname,
"..",
"..",
"torrent-client",
"main.py"
);
return cp.spawn("python3", [scriptPath, ...commonArgs], {
stdio: "inherit",
});
}
};

View File

@@ -0,0 +1,144 @@
import cp from "node:child_process";
import { Game } from "@main/entity";
import { RPC_PASSWORD, RPC_PORT, startTorrentClient } from "./torrent-client";
import { gameRepository } from "@main/repository";
import { DownloadProgress } from "@types";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { calculateETA } from "./helpers";
import axios from "axios";
import {
CancelDownloadPayload,
StartDownloadPayload,
PauseDownloadPayload,
LibtorrentStatus,
LibtorrentPayload,
} from "./types";
export class TorrentDownloader {
private static torrentClient: 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,
},
});
private static spawn(args: StartDownloadPayload) {
this.torrentClient = startTorrentClient(args);
}
public static kill() {
if (this.torrentClient) {
this.torrentClient.kill();
this.torrentClient = null;
this.downloadingGameId = -1;
}
}
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) {
this.downloadingGameId = -1;
}
return {
numPeers,
numSeeds,
downloadSpeed,
timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed),
isDownloadingMetadata,
isCheckingFiles,
progress,
gameId,
} as DownloadProgress;
} catch (err) {
return null;
}
}
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.torrentClient) {
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);
}
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;
}
}

View File

@@ -0,0 +1,33 @@
export interface StartDownloadPayload {
game_id: number;
magnet: string;
save_path: string;
}
export interface PauseDownloadPayload {
game_id: number;
}
export interface CancelDownloadPayload {
game_id: number;
}
export enum LibtorrentStatus {
CheckingFiles = 1,
DownloadingMetadata = 2,
Downloading = 3,
Finished = 4,
Seeding = 5,
}
export interface LibtorrentPayload {
progress: number;
numPeers: number;
numSeeds: number;
downloadSpeed: number;
bytesDownloaded: number;
fileSize: number;
folderName: string;
status: LibtorrentStatus;
gameId: number;
}

View File

@@ -90,7 +90,21 @@ export class HydraApi {
return response; return response;
}, },
(error) => { (error) => {
logger.error("response error", error); logger.error(" ---- RESPONSE ERROR -----");
const { config } = error;
logger.error(config.method, config.baseURL, config.url, config.headers);
if (error.response) {
logger.error(error.response.status, error.response.data);
} else if (error.request) {
logger.error(error.request);
} else {
logger.error("Error", error.message);
}
logger.error(" ----- END RESPONSE ERROR -------");
return Promise.reject(error); return Promise.reject(error);
} }
); );
@@ -106,10 +120,17 @@ export class HydraApi {
}; };
} }
private static sendSignOutEvent() {
if (WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send("on-signout");
}
}
private static async revalidateAccessTokenIfExpired() { private static async revalidateAccessTokenIfExpired() {
if (!this.userAuth.authToken) { if (!this.userAuth.authToken) {
userAuthRepository.delete({ id: 1 }); userAuthRepository.delete({ id: 1 });
logger.error("user is not logged in"); logger.error("user is not logged in");
this.sendSignOutEvent();
throw new Error("user is not logged in"); throw new Error("user is not logged in");
} }
@@ -139,26 +160,7 @@ export class HydraApi {
["id"] ["id"]
); );
} catch (err) { } catch (err) {
if ( this.handleUnauthorizedError(err);
err instanceof AxiosError &&
(err?.response?.status === 401 || err?.response?.status === 403)
) {
this.userAuth = {
authToken: "",
expirationTimestamp: 0,
refreshToken: "",
};
userAuthRepository.delete({ id: 1 });
if (WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send("on-signout");
}
logger.log("user refresh token expired");
}
throw err;
} }
} }
} }
@@ -171,28 +173,54 @@ export class HydraApi {
}; };
} }
private static handleUnauthorizedError = (err) => {
if (err instanceof AxiosError && err.response?.status === 401) {
this.userAuth = {
authToken: "",
expirationTimestamp: 0,
refreshToken: "",
};
userAuthRepository.delete({ id: 1 });
this.sendSignOutEvent();
}
throw err;
};
static async get(url: string) { static async get(url: string) {
await this.revalidateAccessTokenIfExpired(); await this.revalidateAccessTokenIfExpired();
return this.instance.get(url, this.getAxiosConfig()); return this.instance
.get(url, this.getAxiosConfig())
.catch(this.handleUnauthorizedError);
} }
static async post(url: string, data?: any) { static async post(url: string, data?: any) {
await this.revalidateAccessTokenIfExpired(); await this.revalidateAccessTokenIfExpired();
return this.instance.post(url, data, this.getAxiosConfig()); return this.instance
.post(url, data, this.getAxiosConfig())
.catch(this.handleUnauthorizedError);
} }
static async put(url: string, data?: any) { static async put(url: string, data?: any) {
await this.revalidateAccessTokenIfExpired(); await this.revalidateAccessTokenIfExpired();
return this.instance.put(url, data, this.getAxiosConfig()); return this.instance
.put(url, data, this.getAxiosConfig())
.catch(this.handleUnauthorizedError);
} }
static async patch(url: string, data?: any) { static async patch(url: string, data?: any) {
await this.revalidateAccessTokenIfExpired(); await this.revalidateAccessTokenIfExpired();
return this.instance.patch(url, data, this.getAxiosConfig()); return this.instance
.patch(url, data, this.getAxiosConfig())
.catch(this.handleUnauthorizedError);
} }
static async delete(url: string) { static async delete(url: string) {
await this.revalidateAccessTokenIfExpired(); await this.revalidateAccessTokenIfExpired();
return this.instance.delete(url, this.getAxiosConfig()); return this.instance
.delete(url, this.getAxiosConfig())
.catch(this.handleUnauthorizedError);
} }
} }

View File

@@ -3,7 +3,7 @@ export * from "./steam";
export * from "./steam-250"; export * from "./steam-250";
export * from "./steam-grid"; export * from "./steam-grid";
export * from "./window-manager"; export * from "./window-manager";
export * from "./download-manager"; export * from "./download";
export * from "./how-long-to-beat"; export * from "./how-long-to-beat";
export * from "./process-watcher"; export * from "./process-watcher";
export * from "./main-loop"; export * from "./main-loop";

View File

@@ -64,7 +64,7 @@ export const mergeWithRemoteGames = async () => {
} }
} catch (err) { } catch (err) {
if (err instanceof AxiosError) { if (err instanceof AxiosError) {
logger.error("getRemoteGames", err.response, err.message); logger.error("getRemoteGames", err.message);
} else { } else {
logger.error("getRemoteGames", err); logger.error("getRemoteGames", err);
} }

View File

@@ -1,5 +1,5 @@
import { sleep } from "@main/helpers"; import { sleep } from "@main/helpers";
import { DownloadManager } from "./download-manager"; import { DownloadManager } from "./download";
import { watchProcesses } from "./process-watcher"; import { watchProcesses } from "./process-watcher";
export const startMainLoop = async () => { export const startMainLoop = async () => {

View File

@@ -95,6 +95,7 @@ export class WindowManager {
minimizable: false, minimizable: false,
webPreferences: { webPreferences: {
sandbox: false, sandbox: false,
nodeIntegrationInSubFrames: true,
}, },
}); });

View File

@@ -3,6 +3,7 @@
interface ImportMetaEnv { interface ImportMetaEnv {
readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string; readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string;
readonly MAIN_VITE_API_URL: string; readonly MAIN_VITE_API_URL: string;
readonly MAIN_VITE_SENTRY_DSN: string;
} }
interface ImportMeta { interface ImportMeta {

View File

@@ -110,6 +110,7 @@ contextBridge.exposeInMainWorld("electron", {
ping: () => ipcRenderer.invoke("ping"), ping: () => ipcRenderer.invoke("ping"),
getVersion: () => ipcRenderer.invoke("getVersion"), getVersion: () => ipcRenderer.invoke("getVersion"),
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"), getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
isPortableVersion: () => ipcRenderer.invoke("isPortableVersion"),
openExternal: (src: string) => ipcRenderer.invoke("openExternal", src), openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
isUserLoggedIn: () => ipcRenderer.invoke("isUserLoggedIn"), isUserLoggedIn: () => ipcRenderer.invoke("isUserLoggedIn"),
showOpenDialog: (options: Electron.OpenDialogOptions) => showOpenDialog: (options: Electron.OpenDialogOptions) =>

View File

@@ -6,7 +6,7 @@
<title>Hydra</title> <title>Hydra</title>
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: https://*.s3.amazonaws.com https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' local: data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: https://*.cloudfront.net https://*.s3.amazonaws.com https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' local: data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;"
/> />
</head> </head>
<body style="background-color: #1c1c1c"> <body style="background-color: #1c1c1c">

View File

@@ -32,8 +32,17 @@ export function BottomPanel() {
const status = useMemo(() => { const status = useMemo(() => {
if (isGameDownloading) { if (isGameDownloading) {
if (lastPacket?.isCheckingFiles)
return t("checking_files", {
title: lastPacket?.game.title,
percentage: progress,
});
if (lastPacket?.isDownloadingMetadata) if (lastPacket?.isDownloadingMetadata)
return t("downloading_metadata", { title: lastPacket?.game.title }); return t("downloading_metadata", {
title: lastPacket?.game.title,
percentage: progress,
});
if (!eta) { if (!eta) {
return t("calculating_eta", { return t("calculating_eta", {
@@ -56,6 +65,7 @@ export function BottomPanel() {
isGameDownloading, isGameDownloading,
lastPacket?.game, lastPacket?.game,
lastPacket?.isDownloadingMetadata, lastPacket?.isDownloadingMetadata,
lastPacket?.isCheckingFiles,
progress, progress,
eta, eta,
downloadSpeed, downloadSpeed,

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
@@ -14,6 +14,7 @@ import { buildGameDetailsPath } from "@renderer/helpers";
import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { SidebarProfile } from "./sidebar-profile"; import { SidebarProfile } from "./sidebar-profile";
import { sortBy } from "lodash-es";
const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250; const SIDEBAR_INITIAL_WIDTH = 250;
@@ -35,6 +36,10 @@ export function Sidebar() {
const location = useLocation(); const location = useLocation();
const sortedLibrary = useMemo(() => {
return sortBy(library, (game) => game.title);
}, [library]);
const { lastPacket, progress } = useDownload(); const { lastPacket, progress } = useDownload();
const { showWarningToast } = useToast(); const { showWarningToast } = useToast();
@@ -43,7 +48,7 @@ export function Sidebar() {
updateLibrary(); updateLibrary();
}, [lastPacket?.game.id, updateLibrary]); }, [lastPacket?.game.id, updateLibrary]);
const isDownloading = library.some( const isDownloading = sortedLibrary.some(
(game) => game.status === "active" && game.progress !== 1 (game) => game.status === "active" && game.progress !== 1
); );
@@ -63,7 +68,7 @@ export function Sidebar() {
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => { const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
setFilteredLibrary( setFilteredLibrary(
library.filter((game) => sortedLibrary.filter((game) =>
game.title game.title
.toLowerCase() .toLowerCase()
.includes(event.target.value.toLocaleLowerCase()) .includes(event.target.value.toLocaleLowerCase())
@@ -72,8 +77,8 @@ export function Sidebar() {
}; };
useEffect(() => { useEffect(() => {
setFilteredLibrary(library); setFilteredLibrary(sortedLibrary);
}, [library]); }, [sortedLibrary]);
useEffect(() => { useEffect(() => {
window.onmousemove = (event: MouseEvent) => { window.onmousemove = (event: MouseEvent) => {

View File

@@ -140,7 +140,7 @@ export function GameDetailsContextProvider({
filters: [ filters: [
{ {
name: "Game executable", name: "Game executable",
extensions: ["exe"], extensions: ["exe", "lnk"],
}, },
], ],
}) })

View File

@@ -104,6 +104,7 @@ declare global {
getVersion: () => Promise<string>; getVersion: () => Promise<string>;
ping: () => string; ping: () => string;
getDefaultDownloadsPath: () => Promise<string>; getDefaultDownloadsPath: () => Promise<string>;
isPortableVersion: () => Promise<boolean>;
showOpenDialog: ( showOpenDialog: (
options: Electron.OpenDialogOptions options: Electron.OpenDialogOptions
) => Promise<Electron.OpenDialogReturnValue>; ) => Promise<Electron.OpenDialogReturnValue>;

View File

@@ -22,13 +22,14 @@ export function useDownload() {
); );
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const startDownload = (payload: StartGameDownloadPayload) => const startDownload = (payload: StartGameDownloadPayload) => {
dispatch(clearDownload());
window.electron.startGameDownload(payload).then((game) => { window.electron.startGameDownload(payload).then((game) => {
dispatch(clearDownload());
updateLibrary(); updateLibrary();
return game; return game;
}); });
};
const pauseDownload = async (gameId: number) => { const pauseDownload = async (gameId: number) => {
await window.electron.pauseGameDownload(gameId); await window.electron.pauseGameDownload(gameId);
@@ -65,7 +66,7 @@ export function useDownload() {
updateLibrary(); updateLibrary();
}); });
const getETA = () => { const calculateETA = () => {
if (!lastPacket || lastPacket.timeRemaining < 0) return ""; if (!lastPacket || lastPacket.timeRemaining < 0) return "";
try { try {
@@ -85,9 +86,9 @@ export function useDownload() {
return { return {
downloadSpeed: `${formatBytes(lastPacket?.downloadSpeed ?? 0)}/s`, downloadSpeed: `${formatBytes(lastPacket?.downloadSpeed ?? 0)}/s`,
progress: formatDownloadProgress(lastPacket?.game.progress), progress: formatDownloadProgress(lastPacket?.progress ?? 0),
lastPacket, lastPacket,
eta: getETA(), eta: calculateETA(),
startDownload, startDownload,
pauseDownload, pauseDownload,
resumeDownload, resumeDownload,

View File

@@ -6,6 +6,8 @@ import { Provider } from "react-redux";
import LanguageDetector from "i18next-browser-languagedetector"; import LanguageDetector from "i18next-browser-languagedetector";
import { HashRouter, Route, Routes } from "react-router-dom"; import { HashRouter, Route, Routes } from "react-router-dom";
import * as Sentry from "@sentry/electron/renderer";
import "@fontsource/fira-mono/400.css"; import "@fontsource/fira-mono/400.css";
import "@fontsource/fira-mono/500.css"; import "@fontsource/fira-mono/500.css";
import "@fontsource/fira-mono/700.css"; import "@fontsource/fira-mono/700.css";
@@ -29,6 +31,8 @@ import { store } from "./store";
import * as resources from "@locales"; import * as resources from "@locales";
import { User } from "./pages/user/user"; import { User } from "./pages/user/user";
Sentry.init({});
i18n i18n
.use(LanguageDetector) .use(LanguageDetector)
.use(initReactI18next) .use(initReactI18next)

View File

@@ -67,6 +67,19 @@ export function DownloadGroup({
} }
if (isGameDownloading) { if (isGameDownloading) {
if (lastPacket?.isDownloadingMetadata) {
return <p>{t("downloading_metadata")}</p>;
}
if (lastPacket?.isCheckingFiles) {
return (
<>
<p>{progress}</p>
<p>{t("checking_files")}</p>
</>
);
}
return ( return (
<> <>
<p>{progress}</p> <p>{progress}</p>
@@ -110,7 +123,7 @@ export function DownloadGroup({
); );
} }
return <p>{t(game.status)}</p>; return <p>{t(game.status as string)}</p>;
}; };
const getGameActions = (game: LibraryGame) => { const getGameActions = (game: LibraryGame) => {

View File

@@ -54,7 +54,7 @@ export function HeroPanelPlaytime() {
if (!game) return null; if (!game) return null;
const hasDownload = const hasDownload =
["active", "paused"].includes(game.status) && game.progress !== 1; ["active", "paused"].includes(game.status as string) && game.progress !== 1;
const isGameDownloading = const isGameDownloading =
game.status === "active" && lastPacket?.game.id === game.id; game.status === "active" && lastPacket?.game.id === game.id;

View File

@@ -23,7 +23,7 @@ export function GameOptionsModal({
const { showSuccessToast, showErrorToast } = useToast(); const { showSuccessToast, showErrorToast } = useToast();
const { updateGame, setShowRepacksModal, selectGameExecutable } = const { updateGame, setShowRepacksModal, repacks, selectGameExecutable } =
useContext(gameDetailsContext); useContext(gameDetailsContext);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
@@ -156,7 +156,7 @@ export function GameOptionsModal({
<Button <Button
onClick={() => setShowRepacksModal(true)} onClick={() => setShowRepacksModal(true)}
theme="outline" theme="outline"
disabled={deleting || isGameDownloading} disabled={deleting || isGameDownloading || !repacks.length}
> >
{t("open_download_options")} {t("open_download_options")}
</Button> </Button>

View File

@@ -10,6 +10,8 @@ export function SettingsBehavior() {
(state) => state.userPreferences.value (state) => state.userPreferences.value
); );
const [showRunAtStartup, setShowRunAtStartup] = useState(false);
const { updateUserPreferences } = useContext(settingsContext); const { updateUserPreferences } = useContext(settingsContext);
const [form, setForm] = useState({ const [form, setForm] = useState({
@@ -28,6 +30,12 @@ export function SettingsBehavior() {
} }
}, [userPreferences]); }, [userPreferences]);
useEffect(() => {
window.electron.isPortableVersion().then((isPortableVersion) => {
setShowRunAtStartup(!isPortableVersion);
});
}, []);
const handleChange = (values: Partial<typeof form>) => { const handleChange = (values: Partial<typeof form>) => {
setForm((prev) => ({ ...prev, ...values })); setForm((prev) => ({ ...prev, ...values }));
updateUserPreferences(values); updateUserPreferences(values);
@@ -45,14 +53,16 @@ export function SettingsBehavior() {
} }
/> />
<CheckboxField {showRunAtStartup && (
label={t("launch_with_system")} <CheckboxField
onChange={() => { label={t("launch_with_system")}
handleChange({ runAtStartup: !form.runAtStartup }); onChange={() => {
window.electron.autoLaunch(!form.runAtStartup); handleChange({ runAtStartup: !form.runAtStartup });
}} window.electron.autoLaunch(!form.runAtStartup);
checked={form.runAtStartup} }}
/> checked={form.runAtStartup}
/>
)}
</> </>
); );
} }

View File

@@ -1,6 +1,6 @@
import { UserProfile } from "@types"; import { UserProfile } from "@types";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { setHeaderTitle } from "@renderer/features"; import { setHeaderTitle } from "@renderer/features";
import { useAppDispatch } from "@renderer/hooks"; import { useAppDispatch } from "@renderer/hooks";
import { UserSkeleton } from "./user-skeleton"; import { UserSkeleton } from "./user-skeleton";
@@ -12,6 +12,7 @@ import * as styles from "./user.css";
export const User = () => { export const User = () => {
const { userId } = useParams(); const { userId } = useParams();
const [userProfile, setUserProfile] = useState<UserProfile>(); const [userProfile, setUserProfile] = useState<UserProfile>();
const navigate = useNavigate();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@@ -20,6 +21,8 @@ export const User = () => {
if (userProfile) { if (userProfile) {
dispatch(setHeaderTitle(userProfile.displayName)); dispatch(setHeaderTitle(userProfile.displayName));
setUserProfile(userProfile); setUserProfile(userProfile);
} else {
navigate(-1);
} }
}); });
}, [dispatch, userId]); }, [dispatch, userId]);

View File

@@ -1,6 +1,2 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" /> /// <reference types="vite-plugin-svgr/client" />
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -1,6 +1,13 @@
import type { Aria2Status } from "aria2";
import type { DownloadSourceStatus, Downloader } from "@shared"; import type { DownloadSourceStatus, Downloader } from "@shared";
export type GameStatus =
| "active"
| "waiting"
| "paused"
| "error"
| "complete"
| "removed";
export type GameShop = "steam" | "epic"; export type GameShop = "steam" | "epic";
export interface SteamGenre { export interface SteamGenre {
@@ -106,7 +113,7 @@ export interface Game {
id: number; id: number;
title: string; title: string;
iconUrl: string; iconUrl: string;
status: Aria2Status | null; status: GameStatus | null;
folderName: string; folderName: string;
downloadPath: string | null; downloadPath: string | null;
repacks: GameRepack[]; repacks: GameRepack[];
@@ -142,6 +149,9 @@ export interface DownloadProgress {
numPeers: number; numPeers: number;
numSeeds: number; numSeeds: number;
isDownloadingMetadata: boolean; isDownloadingMetadata: boolean;
isCheckingFiles: boolean;
progress: number;
gameId: number;
game: LibraryGame; game: LibraryGame;
} }
@@ -262,6 +272,7 @@ export interface UserDetails {
export interface UserProfile { export interface UserProfile {
id: string; id: string;
displayName: string; displayName: string;
username: string;
profileImageUrl: string | null; profileImageUrl: string | null;
totalPlayTimeInSeconds: number; totalPlayTimeInSeconds: number;
libraryGames: UserGame[]; libraryGames: UserGame[];

View File

@@ -0,0 +1,52 @@
import libtorrent as lt
class Downloader:
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)})
def start_download(self, game_id: int, magnet: str, save_path: str):
params = {'url': magnet, 'save_path': save_path}
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 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()
return {
'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,
}

61
torrent-client/main.py Normal file
View File

@@ -0,0 +1,61 @@
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import urllib.parse
from downloader import Downloader
torrent_port = sys.argv[1]
http_port = sys.argv[2]
rpc_password = sys.argv[3]
initial_download = json.loads(urllib.parse.unquote(sys.argv[4]))
downloader = Downloader(torrent_port)
downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path'])
class Handler(BaseHTTPRequestHandler):
rpc_password_header = 'x-hydra-rpc-password'
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 = downloader.get_download_status()
self.wfile.write(json.dumps(status).encode('utf-8'))
if self.path == "/healthcheck":
self.send_response(200)
self.end_headers()
def do_POST(self):
if self.path == "/action":
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 data['action'] == 'start':
downloader.start_download(data['game_id'], data['magnet'], data['save_path'])
elif data['action'] == 'pause':
downloader.pause_download(data['game_id'])
elif data['action'] == 'cancel':
downloader.cancel_download(data['game_id'])
self.send_response(200)
self.end_headers()
if __name__ == "__main__":
httpd = HTTPServer(("", int(http_port)), Handler)
httpd.serve_forever()

20
torrent-client/setup.py Normal file
View File

@@ -0,0 +1,20 @@
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"
)]
)

2870
yarn.lock

File diff suppressed because it is too large Load Diff