mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
Compare commits
251 Commits
v3.3.0
...
feat/httpd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91bb371e0b | ||
|
|
e507676088 | ||
|
|
f3c7010930 | ||
|
|
66d40c566b | ||
|
|
2452a3a51a | ||
|
|
4520f6bb20 | ||
|
|
d7b5bb5940 | ||
|
|
f2efbfba3a | ||
|
|
b7bc5e8cb2 | ||
|
|
ff447c0ed3 | ||
|
|
d1c40b061e | ||
|
|
e6a9ea6139 | ||
|
|
b519275800 | ||
|
|
d56cedb08f | ||
|
|
8e8c79185c | ||
|
|
dd854121f7 | ||
|
|
e4776c8e16 | ||
|
|
e63738ca7b | ||
|
|
3756249013 | ||
|
|
33713cfb9f | ||
|
|
41049be522 | ||
|
|
d9747b7985 | ||
|
|
e0c6d2fe3d | ||
|
|
8db03bcccf | ||
|
|
50acc33a41 | ||
|
|
81936d076e | ||
|
|
7d61d1219d | ||
|
|
19613b69cc | ||
|
|
42a768f715 | ||
|
|
456c0ad6ff | ||
|
|
76181342f9 | ||
|
|
8f95fa70d4 | ||
|
|
4743b9f082 | ||
|
|
db92ef255d | ||
|
|
d1cdfc0ba5 | ||
|
|
77a4642b7b | ||
|
|
148e272c4d | ||
|
|
3bdd8b90d4 | ||
|
|
6569b66801 | ||
|
|
4a11d741eb | ||
|
|
6e8a844a92 | ||
|
|
3821b9836c | ||
|
|
57390c814b | ||
|
|
007fa6f009 | ||
|
|
009cb1d7d7 | ||
|
|
306b49eaf3 | ||
|
|
be232d88e4 | ||
|
|
e3670f5b5a | ||
|
|
bd018399fb | ||
|
|
975eec96be | ||
|
|
44b711f674 | ||
|
|
539ff34b69 | ||
|
|
f99da1d7bf | ||
|
|
75c3bbf858 | ||
|
|
afa78e4634 | ||
|
|
ee1dda90d9 | ||
|
|
5b62b9c593 | ||
|
|
4d76182f2e | ||
|
|
85fb57527a | ||
|
|
9e6b6be0b9 | ||
|
|
3c3f77fc50 | ||
|
|
614cb8a297 | ||
|
|
ba3f010576 | ||
|
|
8c442e742a | ||
|
|
555b3dbb1d | ||
|
|
d2a868b504 | ||
|
|
e27536c6b3 | ||
|
|
cd367faec2 | ||
|
|
087dd9fb2e | ||
|
|
c5d8403843 | ||
|
|
8e01142225 | ||
|
|
a0ef59a13c | ||
|
|
13d5e4469f | ||
|
|
22e92eb8f6 | ||
|
|
d28bb825a3 | ||
|
|
96d59a0fd7 | ||
|
|
84600ea0dc | ||
|
|
9264fa3664 | ||
|
|
5b0ea980de | ||
|
|
622fc393fc | ||
|
|
f76ba5975d | ||
|
|
4da0dac0e6 | ||
|
|
7c468ac9bb | ||
|
|
2ee3bebfc7 | ||
|
|
98ed07d6d2 | ||
|
|
97cf02577a | ||
|
|
fbce53d61a | ||
|
|
1835adf8b4 | ||
|
|
a4dd037cba | ||
|
|
30be12afeb | ||
|
|
258f891a12 | ||
|
|
02662fa993 | ||
|
|
1746f14adb | ||
|
|
7fd0894b56 | ||
|
|
3bb8c17b16 | ||
|
|
417bb69b86 | ||
|
|
a55b5dafa0 | ||
|
|
b7bec47a2c | ||
|
|
f565226a6d | ||
|
|
e7427b8184 | ||
|
|
cc505acbdc | ||
|
|
934831eaae | ||
|
|
a35dae3e18 | ||
|
|
5e58762151 | ||
|
|
21535d8acc | ||
|
|
6106325f0a | ||
|
|
3ff9dd705e | ||
|
|
04f061afa3 | ||
|
|
f8ea4f29d9 | ||
|
|
1520e2b2ae | ||
|
|
d29fc50f68 | ||
|
|
ede538392f | ||
|
|
f464850c38 | ||
|
|
379e90568a | ||
|
|
01d440590b | ||
|
|
56da86d63d | ||
|
|
36cf2060c1 | ||
|
|
5fa4d128c3 | ||
|
|
d15ef33a86 | ||
|
|
54ab719935 | ||
|
|
9eb909cd17 | ||
|
|
b2b4b1ff3c | ||
|
|
f547b85df7 | ||
|
|
0d75878b07 | ||
|
|
9470068de6 | ||
|
|
74a92f57bd | ||
|
|
73f4b0e869 | ||
|
|
e6fde48dbd | ||
|
|
554d195d5b | ||
|
|
fc899197b4 | ||
|
|
7597933aa9 | ||
|
|
3a36bfb75c | ||
|
|
93adb070e5 | ||
|
|
9df58b9918 | ||
|
|
9b8a0af8e9 | ||
|
|
31319b0811 | ||
|
|
9976812bf7 | ||
|
|
6b4d422b65 | ||
|
|
0e164acdf2 | ||
|
|
fffa2d2c01 | ||
|
|
2c35d7da13 | ||
|
|
9a81a45738 | ||
|
|
d284fc914e | ||
|
|
b867eaa34b | ||
|
|
f353d1fc9b | ||
|
|
b03d2a78f8 | ||
|
|
1a6cb5f866 | ||
|
|
fbc81db2d0 | ||
|
|
663e5a46ba | ||
|
|
bc07f73387 | ||
|
|
a697d4ec8b | ||
|
|
008b88f888 | ||
|
|
0e7e5ba920 | ||
|
|
7c98f4afa7 | ||
|
|
12d6b744d7 | ||
|
|
ff7c15a013 | ||
|
|
16d192443f | ||
|
|
0c6b3f2254 | ||
|
|
f92d6839c8 | ||
|
|
d3e383bc1b | ||
|
|
abec986cc1 | ||
|
|
135eef986a | ||
|
|
ec9db3a48f | ||
|
|
72cde368f5 | ||
|
|
bd77aee3c2 | ||
|
|
dafdda1bc7 | ||
|
|
6c6a238c5f | ||
|
|
0df18022ed | ||
|
|
6626368613 | ||
|
|
cff728bbbe | ||
|
|
3961b53d0e | ||
|
|
3f08e87eda | ||
|
|
b84c34a69c | ||
|
|
c2e1a092c9 | ||
|
|
fe03894054 | ||
|
|
893695eda0 | ||
|
|
28727600fe | ||
|
|
d892ed4f34 | ||
|
|
ea618febe2 | ||
|
|
358eac061f | ||
|
|
33f9e5729b | ||
|
|
2a7feba5dc | ||
|
|
1390f99895 | ||
|
|
daf8ee6ba2 | ||
|
|
652fe69141 | ||
|
|
61b2710219 | ||
|
|
0e67e21223 | ||
|
|
f12784b6e7 | ||
|
|
8795359c0b | ||
|
|
bcc2127a6e | ||
|
|
766c5760d7 | ||
|
|
3c8db34456 | ||
|
|
433ebf55ac | ||
|
|
b68bed97dd | ||
|
|
2a8600a841 | ||
|
|
3fd48eb784 | ||
|
|
aa19241b2b | ||
|
|
a16a75ff39 | ||
|
|
e4d97e9775 | ||
|
|
5688aaf0e8 | ||
|
|
ad5eb22553 | ||
|
|
1c3a5f4800 | ||
|
|
d8a7275512 | ||
|
|
489dd710fa | ||
|
|
0db93c7dad | ||
|
|
d9fe3c709e | ||
|
|
03a3d734f1 | ||
|
|
106d66f1a2 | ||
|
|
3ef6472825 | ||
|
|
df77d754ae | ||
|
|
6a8da74bcf | ||
|
|
1aa3bb1ad2 | ||
|
|
fdcf244f4e | ||
|
|
d8a4eaaa66 | ||
|
|
1c71c20b48 | ||
|
|
aefc5eca49 | ||
|
|
5f1b83a0d9 | ||
|
|
85efc23c25 | ||
|
|
b344a1850a | ||
|
|
a9ec1a62bf | ||
|
|
6832c11d06 | ||
|
|
740cc2ec7c | ||
|
|
e12c6daa16 | ||
|
|
2ee8a64945 | ||
|
|
a54edd8b4a | ||
|
|
f7b6a91621 | ||
|
|
96dbfe8593 | ||
|
|
dddc5bfd96 | ||
|
|
282bb45869 | ||
|
|
c982ebe33a | ||
|
|
08935ceabd | ||
|
|
0e7a0d1afa | ||
|
|
38dc8bdea5 | ||
|
|
c33aa3c874 | ||
|
|
afa34c74d7 | ||
|
|
cda9b8fb42 | ||
|
|
026abd7909 | ||
|
|
bc31a28eee | ||
|
|
f1624441b4 | ||
|
|
358abde963 | ||
|
|
c65fc15d99 | ||
|
|
b32226273e | ||
|
|
2e7a2a05a5 | ||
|
|
ab70ff10be | ||
|
|
6a429f9f39 | ||
|
|
b03c69b185 | ||
|
|
3fe77f4961 | ||
|
|
79be151de0 | ||
|
|
e804b92f30 | ||
|
|
cf31b36961 | ||
|
|
3156baf78e |
@@ -1,2 +1,4 @@
|
||||
MAIN_VITE_API_URL=API_URL
|
||||
MAIN_VITE_AUTH_URL=AUTH_URL
|
||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID=
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE=
|
||||
|
||||
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
@@ -11,6 +11,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -31,6 +32,16 @@ jobs:
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
|
||||
- name: Build Rust
|
||||
run: cargo build --release
|
||||
working-directory: ./rust_rpc
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements.txt
|
||||
|
||||
@@ -47,12 +58,13 @@ jobs:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
@@ -61,14 +73,15 @@ jobs:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||
|
||||
- name: Test Upload build
|
||||
- name: Upload build
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
|
||||
@@ -78,7 +91,6 @@ jobs:
|
||||
BUILDS_URL: ${{ secrets.BUILDS_URL }}
|
||||
BUILD_WEBHOOK_URL: ${{ secrets.BUILD_WEBHOOK_URL }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
|
||||
run: node scripts/upload-build.cjs
|
||||
|
||||
- name: Create artifact
|
||||
|
||||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@@ -33,6 +33,16 @@ jobs:
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
|
||||
- name: Build Rust
|
||||
run: cargo build --release
|
||||
working-directory: ./rust_rpc
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements.txt
|
||||
|
||||
@@ -49,13 +59,13 @@ jobs:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
@@ -64,13 +74,13 @@ jobs:
|
||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
|
||||
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
|
||||
|
||||
- name: Create artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
13
.github/workflows/trigger-lp.yml
vendored
Normal file
13
.github/workflows/trigger-lp.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Trigger Landing Page Build
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Trigger Landing Page build
|
||||
run: curl --location --request POST '${{ secrets.LP_TRIGGER_DEPLOY_URL }}'
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -9,10 +9,12 @@ out
|
||||
.vite
|
||||
ludusavi/
|
||||
hydra-python-rpc/
|
||||
aria2/
|
||||
.python-version
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
|
||||
*storybook.log
|
||||
|
||||
|
||||
target/
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
[](./docs/README.cs.md)
|
||||
[](./docs/README.da.md)
|
||||
[](./docs/README.nb.md)
|
||||
[](./docs/README.et.md)
|
||||
[](./docs/README.et.md)
|
||||
[](./docs/README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
BIN
binaries/7z.dll
Normal file
BIN
binaries/7z.dll
Normal file
Binary file not shown.
BIN
binaries/7z.exe
Normal file
BIN
binaries/7z.exe
Normal file
Binary file not shown.
BIN
binaries/7zz
Normal file
BIN
binaries/7zz
Normal file
Binary file not shown.
BIN
binaries/7zzs
Normal file
BIN
binaries/7zzs
Normal file
Binary file not shown.
BIN
binaries/hydra-httpdl.exe
Normal file
BIN
binaries/hydra-httpdl.exe
Normal file
Binary file not shown.
@@ -26,6 +26,7 @@
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
[](README.et.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
[](README.cs.md)
|
||||
[](README.nb.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
194
docs/README.tr.md
Normal file
194
docs/README.tr.md
Normal file
@@ -0,0 +1,194 @@
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[<img src="../resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
|
||||
|
||||
<h1 align="center">Hydra Launcher</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Hydra, kendi gömülü BitTorrent istemcisine sahip bir oyun başlatıcısıdır.</strong>
|
||||
</p>
|
||||
|
||||
[](https://github.com/hydralauncher/hydra/actions)
|
||||
[](https://github.com/hydralauncher/hydra/releases)
|
||||
|
||||
[](README.pt-BR.md)
|
||||
[](../README.md)
|
||||
[](README.ru.md)
|
||||
[](README.uk-UA.md)
|
||||
[](README.be.md)
|
||||
[](README.es.md)
|
||||
[](README.fr.md)
|
||||
[](README.de.md)
|
||||
[](README.it.md)
|
||||
[](README.cs.md)
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## <a name="içindekiler"></a> İçindekiler
|
||||
|
||||
- [İçindekiler](#içindekiler)
|
||||
- [Hakkında](#hakkında)
|
||||
- [Özellikler](#özellikler)
|
||||
- [Kurulum](#kurulum)
|
||||
- [Katkıda bulunma](#katkıda-bulunma)
|
||||
- [Telegram grubumuza katılın](#telegram-katıl)
|
||||
- [Repoyu forklayın ve klonlayın](#repo-fork-klon)
|
||||
- [Katkıda bulunabileceğin yollar](#katkı-yolları)
|
||||
- [Proje yapısı](#proje-yapısı)
|
||||
- [Kaynak kodundan derleme](#kaynak-kodundan-derleme)
|
||||
- [Node.js'i yükleme](#nodejs-yükle)
|
||||
- [Yarn'ı yükleme](#yarn-yükle)
|
||||
- [Node bağımlılıklarını yükleme](#node-bağımlılık-yükle)
|
||||
- [OpenSSL 1.1'i yükleme](#openssl-1-1-yükle)
|
||||
- [Python 3.9'u yükleme](#python-3-9-yükle)
|
||||
- [Python bağımlılıklarını yükleme](#python-bağımlılık-yükle)
|
||||
- [Ortam değişkenleri](#ortam-değişkenleri)
|
||||
- [Çalıştırma](#çalıştırma)
|
||||
- [Derleme](#derleme)
|
||||
- [BitTorrent istemcisini derleme](#bittorrent-istemci-derle)
|
||||
- [Electron uygulamasını derleme](#electron-uygulama-derle)
|
||||
- [Katkıda bulunanlar](#katkıda-bulunanlar)
|
||||
- [Lisans](#lisans)
|
||||
|
||||
## <a name="hakkında"></a> Hakkında
|
||||
|
||||
**Hydra**, kendi gömülü **BitTorrent istemci**sine sahip bir **oyun başlatıcısı**dır.
|
||||
<br>
|
||||
Başlatıcı, torrent sistemini libtorrent kullanarak yöneten Python ve TypeScript (Electron) ile yazılmıştır.
|
||||
|
||||
## <a name="özellikler"></a> Özellikler
|
||||
|
||||
- Kendi gömülü BitTorrent istemcisi
|
||||
- Oyun sayfasında How Long To Beat (HLTB) entegrasyonu
|
||||
- İndirme yolu özelleştirmesi
|
||||
- Windows ve Linux desteği
|
||||
- Sürekli güncelleme
|
||||
- Ve daha fazlası...
|
||||
|
||||
## <a name="kurulum"></a> Kurulum
|
||||
|
||||
Aşağıdaki adımları izleyerek Hydra'yı kurun:
|
||||
|
||||
1. Hydra'nın en son sürümünü [Releases](https://github.com/hydralauncher/hydra/releases/latest) sayfasından indirin.
|
||||
- Hydra'yı Windows'a kurmak istiyorsanız sadece .exe dosyasını indirin.
|
||||
- Hydra'yı Linux'a kurmak istiyorsanız .deb, .rpm veya .zip dosyasını indirin (kullandığınız Linux dağıtımına bağlı olarak).
|
||||
2. İndirilen dosyayı çalıştırın.
|
||||
3. Hydra'nın keyfini çıkarın!
|
||||
|
||||
## <a name="katkıda-bulunma"></a> Katkıda Bulunma
|
||||
|
||||
### <a name="telegram-katıl"></a> Telegram grubumuza katılın
|
||||
|
||||
Tartışmalarımızı [Telegram](https://t.me/hydralauncher) kanalımız üzerinde yürütüyoruz.
|
||||
|
||||
### <a name="repo-fork-klon"></a> Repoyu forklayın ve klonlayın
|
||||
|
||||
1. Depoyu fork'layın [(şimdi forklamak için tıklayın)](https://github.com/hydralauncher/hydra/fork)
|
||||
2. Forkladığınız kodu klonlayın `git clone https://github.com/kullanıcı_adınız/hydra`
|
||||
3. Yeni bir branch oluşturun
|
||||
4. Commitlerinizi gönderin (push)
|
||||
5. Yeni bir Pull Request gönderin
|
||||
|
||||
### <a name="katkı-yolları"></a> Katkıda bulunabileceğin yollar
|
||||
|
||||
- Çeviri: Hydra'nın mümkün olduğunca fazla kişiye ulaşmasını istiyoruz. Yeni dillere çeviri yapmak ya da mevcut dillere güncelleme ve iyileştirme yapmak için yardımcı olmaktan çekinmeyin.
|
||||
- Kod: Hydra, Typescript, Electron ve biraz Python ile inşa edilmiştir. Katkıda bulunmak isterseniz, [Telegram](https://t.me/hydralauncher) kanalımıza katılın!
|
||||
|
||||
### <a name="proje-yapısı"></a> Proje yapısı
|
||||
|
||||
- torrent-client: Torrent indirmelerini yönetmek için libtorrent adlı bir Python kütüphanesini kullanıyoruz.
|
||||
- src/renderer: Uygulamanın kullanıcı arayüzü burada bulunur.
|
||||
- src/main: Uygulamanın tüm işleyişi ve iş mantığı bu bölümde bulunur.
|
||||
|
||||
## <a name="kaynak-kodundan-derleme"></a> Kaynak kodundan derleme
|
||||
|
||||
### <a name="nodejs-yükle"></a> Node.js'i yükleme
|
||||
|
||||
Makinenizde Node.js'in yüklü olduğundan emin olun. Yüklü değilse, [nodejs.org](https://nodejs.org/) adresinden indirip kurun.
|
||||
|
||||
### <a name="yarn-yükle"></a> Yarn'ı yükleme
|
||||
|
||||
Yarn, Node.js için bir paket yöneticisidir. Eğer Yarn'ı henüz kurmadıysanız, [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/) adresindeki talimatları izleyerek kurabilirsiniz.
|
||||
|
||||
### <a name="node-bağımlılık-yükle"></a> Node bağımlılıklarını yükleme
|
||||
|
||||
Proje dizinine gidin ve Yarn kullanarak Node bağımlılıklarını yükleyin:
|
||||
|
||||
```bash
|
||||
cd hydra
|
||||
yarn
|
||||
```
|
||||
|
||||
### <a name="openssl-1-1-yükle"></a> OpenSSL 1.1'i yükleme
|
||||
|
||||
Windows ortamlarında libtorrent tarafından gerekli olan [OpenSSL 1.1](https://slproweb.com/download/Win64OpenSSL-1_1_1w.exe)'i indirip yükleyin.
|
||||
|
||||
### <a name="python-3-9-yükle"></a> Python 3.9'u yükleme
|
||||
|
||||
Makinenizde Python 3.9'un yüklü olduğundan emin olun. Bunu [python.org](https://www.python.org/downloads/release/python-3913/) adresinden indirip kurarak yapabilirsiniz.
|
||||
|
||||
### <a name="python-bağımlılık-yükle"></a> Python bağımlılıklarını yükleme
|
||||
|
||||
Gerekli Python bağımlılıklarını pip kullanarak yükleyin:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## <a name="ortam-değişkenleri"></a> Ortam değişkenleri
|
||||
|
||||
Oyun simgelerini yüklemek için bir SteamGridDB API Anahtarına ihtiyacınız olacak.
|
||||
|
||||
Bu anahtara sahip olduktan sonra, `.env.example` dosyasını kopyalayabilir veya adını `.env` olarak değiştirebilir ve `STEAMGRIDDB_API_KEY` değerini buraya ekleyebilirsiniz.
|
||||
|
||||
## <a name="çalıştırma"></a> Çalıştırma
|
||||
|
||||
Tüm ayarları tamamladıktan sonra, hem Electron sürecini hem de bittorrent istemcisini başlatmak için aşağıdaki komutu çalıştırabilirsiniz:
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## <a name="derleme"></a> Derleme
|
||||
|
||||
### <a name="bittorrent-istemci-derle"></a> BitTorrent istemcisini derleme
|
||||
|
||||
Bittorrent istemcisini aşağıdaki komutla derleyin:
|
||||
|
||||
```bash
|
||||
python torrent-client/setup.py build
|
||||
```
|
||||
|
||||
### <a name="electron-uygulama-derle"></a> Electron uygulamasını derleme
|
||||
|
||||
Electron uygulamasını aşağıdaki komutlarla derleyebilirsiniz:
|
||||
|
||||
Windows'ta:
|
||||
|
||||
```bash
|
||||
yarn build:win
|
||||
```
|
||||
|
||||
Linux'ta:
|
||||
|
||||
```bash
|
||||
yarn build:linux
|
||||
```
|
||||
|
||||
## <a name="katkıda-bulunanlar"></a> Katkıda bulunanlar
|
||||
|
||||
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
|
||||
</a>
|
||||
|
||||
## <a name="lisans"></a> Lisans
|
||||
|
||||
Hydra, [MIT Lisansı](https://github.com/hydralauncher/hydra/blob/main/LICENSE) altında lisanlanmıştır.
|
||||
@@ -26,6 +26,7 @@
|
||||
[](README.da.md)
|
||||
[](README.nb.md)
|
||||
[](README.et.md)
|
||||
[](README.tr.md)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ productName: Hydra
|
||||
directories:
|
||||
buildResources: build
|
||||
extraResources:
|
||||
- aria2
|
||||
- ludusavi
|
||||
- hydra-python-rpc
|
||||
- seeds
|
||||
@@ -20,6 +19,10 @@ asarUnpack:
|
||||
- resources/**
|
||||
win:
|
||||
executableName: Hydra
|
||||
extraResources:
|
||||
- from: binaries/7z.exe
|
||||
- from: binaries/7z.dll
|
||||
- from: rust_rpc/target/release/hydra-httpdl.exe
|
||||
target:
|
||||
- nsis
|
||||
- portable
|
||||
@@ -35,6 +38,9 @@ portable:
|
||||
artifactName: ${name}-${version}-portable.${ext}
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extraResources:
|
||||
- from: binaries/7zz
|
||||
- from: rust_rpc/target/release/hydra-httpdl
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
@@ -44,6 +50,9 @@ mac:
|
||||
dmg:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
linux:
|
||||
extraResources:
|
||||
- from: binaries/7zzs
|
||||
- from: rust_rpc/target/release/hydra-httpdl
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
|
||||
@@ -16,9 +16,6 @@ export default defineConfig(({ mode }) => {
|
||||
main: {
|
||||
build: {
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
external: ["better-sqlite3"],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "3.3.0",
|
||||
"version": "3.4.2",
|
||||
"description": "Hydra",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Los Broxas",
|
||||
@@ -21,15 +21,14 @@
|
||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "electron-vite dev",
|
||||
"dev": "cargo build --manifest-path=rust_rpc/Cargo.toml && electron-vite dev",
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps && node ./scripts/postinstall.cjs",
|
||||
"build:unpack": "npm run build && electron-builder --dir",
|
||||
"build:win": "electron-vite build && electron-builder --win",
|
||||
"build:mac": "electron-vite build && electron-builder --mac",
|
||||
"build:linux": "electron-vite build && electron-builder --linux",
|
||||
"prepare": "husky",
|
||||
"knex:migrate:make": "knex --knexfile src/main/knexfile.ts migrate:make --esm"
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
@@ -44,7 +43,7 @@
|
||||
"@sentry/vite-plugin": "^2.22.7",
|
||||
"auto-launch": "^5.0.6",
|
||||
"axios": "^1.7.9",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"axios-cookiejar-support": "^5.0.5",
|
||||
"classic-level": "^2.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
"color": "^4.2.3",
|
||||
@@ -54,14 +53,13 @@
|
||||
"dexie": "^4.0.10",
|
||||
"diskusage": "^1.2.0",
|
||||
"electron-log": "^5.2.4",
|
||||
"electron-updater": "^6.3.9",
|
||||
"electron-updater": "^6.6.2",
|
||||
"file-type": "^19.6.0",
|
||||
"i18next": "^23.11.2",
|
||||
"i18next-browser-languagedetector": "^7.2.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kill-port": "^2.0.1",
|
||||
"knex": "^3.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"parse-torrent": "^11.0.17",
|
||||
"piscina": "^4.7.0",
|
||||
@@ -75,6 +73,7 @@
|
||||
"sound-play": "^1.1.0",
|
||||
"sudo-prompt": "^9.2.1",
|
||||
"tar": "^7.4.3",
|
||||
"tough-cookie": "^5.1.1",
|
||||
"user-agents": "^1.1.387",
|
||||
"yaml": "^2.6.1",
|
||||
"yup": "^1.5.0",
|
||||
@@ -101,7 +100,7 @@
|
||||
"@types/user-agents": "^1.0.4",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"electron": "^31.7.7",
|
||||
"electron-builder": "^25.1.8",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-vite": "^2.3.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
|
||||
@@ -1,48 +1,94 @@
|
||||
import aria2p
|
||||
import os
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
class HttpDownloader:
|
||||
def __init__(self):
|
||||
self.download = None
|
||||
self.aria2 = aria2p.API(
|
||||
aria2p.Client(
|
||||
host="http://localhost",
|
||||
port=6800,
|
||||
secret=""
|
||||
)
|
||||
)
|
||||
def __init__(self, hydra_httpdl_bin: str):
|
||||
self.hydra_exe = hydra_httpdl_bin
|
||||
self.process = None
|
||||
self.last_status = None
|
||||
|
||||
def start_download(self, url: str, save_path: str, header: str, out: str = None):
|
||||
if self.download:
|
||||
self.aria2.resume([self.download])
|
||||
def start_download(self, url: str, save_path: str, header: str = None, allow_multiple_connections: bool = False, connections_limit: int = 1):
|
||||
cmd = [self.hydra_exe]
|
||||
|
||||
cmd.append(url)
|
||||
|
||||
cmd.extend([
|
||||
"--chunk-size", "64",
|
||||
"--buffer-size", "20",
|
||||
"--force-download",
|
||||
"--log",
|
||||
"--silent"
|
||||
])
|
||||
|
||||
if header:
|
||||
cmd.extend(["--header", header])
|
||||
|
||||
if allow_multiple_connections:
|
||||
cmd.extend(["--connections", str(connections_limit)])
|
||||
else:
|
||||
downloads = self.aria2.add(url, options={"header": header, "dir": save_path, "out": out})
|
||||
|
||||
self.download = downloads[0]
|
||||
|
||||
def pause_download(self):
|
||||
if self.download:
|
||||
self.aria2.pause([self.download])
|
||||
|
||||
def cancel_download(self):
|
||||
if self.download:
|
||||
self.aria2.remove([self.download])
|
||||
self.download = None
|
||||
|
||||
def get_download_status(self):
|
||||
if self.download == None:
|
||||
cmd.extend(["--connections", "1"])
|
||||
|
||||
print(f"running hydra-httpdl: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
self.process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=save_path,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"error running hydra-httpdl: {e}")
|
||||
return None
|
||||
|
||||
download = self.aria2.get_download(self.download.gid)
|
||||
|
||||
response = {
|
||||
'folderName': download.name,
|
||||
'fileSize': download.total_length,
|
||||
'progress': download.completed_length / download.total_length if download.total_length else 0,
|
||||
'downloadSpeed': download.download_speed,
|
||||
'numPeers': 0,
|
||||
'numSeeds': 0,
|
||||
'status': download.status,
|
||||
'bytesDownloaded': download.completed_length,
|
||||
}
|
||||
|
||||
return response
|
||||
def get_download_status(self):
|
||||
|
||||
if not self.process:
|
||||
return None
|
||||
|
||||
try:
|
||||
line = self.process.stdout.readline()
|
||||
if line:
|
||||
status = json.loads(line.strip())
|
||||
self.last_status = status
|
||||
elif self.last_status:
|
||||
status = self.last_status
|
||||
else:
|
||||
return None
|
||||
|
||||
response = {
|
||||
"status": "active",
|
||||
"progress": status["progress"],
|
||||
"downloadSpeed": status["speed_bps"],
|
||||
"numPeers": 0,
|
||||
"numSeeds": 0,
|
||||
"bytesDownloaded": status["downloaded_bytes"],
|
||||
"fileSize": status["total_bytes"],
|
||||
"folderName": status["filename"]
|
||||
}
|
||||
|
||||
if status["progress"] == 1:
|
||||
response["status"] = "complete"
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
print(f"error getting download status: {e}")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def stop_download(self):
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
self.process = None
|
||||
self.last_status = None
|
||||
|
||||
def pause_download(self):
|
||||
self.stop_download()
|
||||
|
||||
def cancel_download(self):
|
||||
self.stop_download()
|
||||
|
||||
@@ -13,6 +13,7 @@ http_port = sys.argv[2]
|
||||
rpc_password = sys.argv[3]
|
||||
start_download_payload = sys.argv[4]
|
||||
start_seeding_payload = sys.argv[5]
|
||||
hydra_httpdl_bin = sys.argv[6]
|
||||
|
||||
downloads = {}
|
||||
# This can be streamed down from Node
|
||||
@@ -32,10 +33,10 @@ if start_download_payload:
|
||||
except Exception as e:
|
||||
print("Error starting torrent download", e)
|
||||
else:
|
||||
http_downloader = HttpDownloader()
|
||||
http_downloader = HttpDownloader(hydra_httpdl_bin)
|
||||
downloads[initial_download['game_id']] = http_downloader
|
||||
try:
|
||||
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
|
||||
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get('allow_multiple_connections', False), initial_download.get('connections_limit', 8))
|
||||
except Exception as e:
|
||||
print("Error starting http download", e)
|
||||
|
||||
@@ -147,11 +148,11 @@ def action():
|
||||
torrent_downloader.start_download(url, data['save_path'])
|
||||
else:
|
||||
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
|
||||
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('allow_multiple_connections', False), data.get('connections_limit', 8))
|
||||
else:
|
||||
http_downloader = HttpDownloader()
|
||||
http_downloader = HttpDownloader(hydra_httpdl_bin)
|
||||
downloads[game_id] = http_downloader
|
||||
http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||
http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('allow_multiple_connections', False), data.get('connections_limit', 8))
|
||||
|
||||
downloading_game_id = game_id
|
||||
|
||||
|
||||
@@ -5,4 +5,3 @@ pywin32; sys_platform == 'win32'
|
||||
psutil
|
||||
Pillow
|
||||
flask
|
||||
aria2p
|
||||
|
||||
2040
rust_rpc/Cargo.lock
generated
Normal file
2040
rust_rpc/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
rust_rpc/Cargo.toml
Normal file
25
rust_rpc/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "hydra-httpdl"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["full", "macros", "rt-multi-thread"] }
|
||||
reqwest = { version = "0.12.5", features = ["stream"] }
|
||||
futures = "0.3"
|
||||
bytes = "1.4"
|
||||
indicatif = "0.17"
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1"
|
||||
tokio-util = { version = "0.7", features = ["io"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
urlencoding = "2.1"
|
||||
serde_json = "1.0"
|
||||
bitvec = "1.0"
|
||||
sha2 = "0.10"
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
982
rust_rpc/src/main.rs
Normal file
982
rust_rpc/src/main.rs
Normal file
@@ -0,0 +1,982 @@
|
||||
use anyhow::Result;
|
||||
use bitvec::prelude::*;
|
||||
use clap::Parser;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use reqwest::{Client, StatusCode, Url};
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
const DEFAULT_MAX_RETRIES: usize = 3;
|
||||
const RETRY_BACKOFF_MS: u64 = 500;
|
||||
const DEFAULT_OUTPUT_FILENAME: &str = "output.bin";
|
||||
const DEFAULT_CONNECTIONS: usize = 16;
|
||||
const DEFAULT_CHUNK_SIZE_MB: usize = 5;
|
||||
const DEFAULT_BUFFER_SIZE_MB: usize = 8;
|
||||
const DEFAULT_VERBOSE: bool = false;
|
||||
const DEFAULT_SILENT: bool = false;
|
||||
const DEFAULT_LOG: bool = false;
|
||||
const DEFAULT_FORCE_NEW: bool = false;
|
||||
const DEFAULT_RESUME_ONLY: bool = false;
|
||||
const DEFAULT_FORCE_DOWNLOAD: bool = false;
|
||||
const HEADER_SIZE: usize = 4096;
|
||||
const MAGIC_NUMBER: &[u8; 5] = b"HYDRA";
|
||||
const FORMAT_VERSION: u8 = 1;
|
||||
// const FINALIZE_BUFFER_SIZE: usize = 1024 * 1024;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "hydra-httpdl")]
|
||||
#[command(author = "los-broxas")]
|
||||
#[command(version = "0.2.0")]
|
||||
#[command(about = "high speed and low resource usage http downloader with resume capability", long_about = None)]
|
||||
struct CliArgs {
|
||||
/// file url to download
|
||||
#[arg(required = true)]
|
||||
url: String,
|
||||
|
||||
/// output file path (or directory to save with original filename)
|
||||
#[arg(default_value = DEFAULT_OUTPUT_FILENAME)]
|
||||
output: String,
|
||||
|
||||
/// number of concurrent connections for parallel download
|
||||
#[arg(short = 'c', long, default_value_t = DEFAULT_CONNECTIONS)]
|
||||
connections: usize,
|
||||
|
||||
/// chunk size in MB for each connection
|
||||
#[arg(short = 'k', long, default_value_t = DEFAULT_CHUNK_SIZE_MB)]
|
||||
chunk_size: usize,
|
||||
|
||||
/// buffer size in MB for file writing
|
||||
#[arg(short, long, default_value_t = DEFAULT_BUFFER_SIZE_MB)]
|
||||
buffer_size: usize,
|
||||
|
||||
/// show detailed progress information
|
||||
#[arg(short = 'v', long, default_value_t = DEFAULT_VERBOSE)]
|
||||
verbose: bool,
|
||||
|
||||
/// suppress progress bar
|
||||
#[arg(short = 's', long, default_value_t = DEFAULT_SILENT)]
|
||||
silent: bool,
|
||||
|
||||
/// log download statistics in JSON format every second
|
||||
#[arg(short = 'l', long, default_value_t = DEFAULT_LOG)]
|
||||
log: bool,
|
||||
|
||||
/// force new download, ignore existing partial files
|
||||
#[arg(short = 'f', long, default_value_t = DEFAULT_FORCE_NEW)]
|
||||
force_new: bool,
|
||||
|
||||
/// only resume existing download, exit if no partial file exists
|
||||
#[arg(short = 'r', long, default_value_t = DEFAULT_RESUME_ONLY)]
|
||||
resume_only: bool,
|
||||
|
||||
/// force download, ignore some verification checks
|
||||
#[arg(short = 'F', long, default_value_t = DEFAULT_FORCE_DOWNLOAD)]
|
||||
force_download: bool,
|
||||
|
||||
/// HTTP headers to send with request (format: "Key: Value")
|
||||
#[arg(short = 'H', long)]
|
||||
header: Vec<String>,
|
||||
}
|
||||
|
||||
struct DownloadConfig {
|
||||
url: String,
|
||||
output_path: String,
|
||||
num_connections: usize,
|
||||
chunk_size: usize,
|
||||
buffer_size: usize,
|
||||
verbose: bool,
|
||||
silent: bool,
|
||||
log: bool,
|
||||
force_new: bool,
|
||||
resume_only: bool,
|
||||
headers: Vec<String>,
|
||||
force_download: bool,
|
||||
}
|
||||
|
||||
impl DownloadConfig {
|
||||
fn should_log(&self) -> bool {
|
||||
self.verbose && !self.silent
|
||||
}
|
||||
|
||||
fn should_log_stats(&self) -> bool {
|
||||
self.log
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadStats {
|
||||
progress_percent: f64,
|
||||
bytes_downloaded: u64,
|
||||
total_size: u64,
|
||||
speed_bytes_per_sec: f64,
|
||||
eta_seconds: u64,
|
||||
elapsed_seconds: u64,
|
||||
}
|
||||
|
||||
struct HydraHeader {
|
||||
magic: [u8; 5], // "HYDRA" identifier
|
||||
version: u8, // header version
|
||||
file_size: u64, // file size
|
||||
etag: [u8; 32], // etag hash
|
||||
url_hash: [u8; 32], // url hash
|
||||
chunk_size: u32, // chunk size
|
||||
chunk_count: u32, // chunk count
|
||||
chunks_bitmap: BitVec<u8>, // chunks bitmap
|
||||
}
|
||||
|
||||
impl HydraHeader {
|
||||
fn new(file_size: u64, etag: &str, url: &str, chunk_size: u32) -> Self {
|
||||
let chunk_count = ((file_size as f64) / (chunk_size as f64)).ceil() as u32;
|
||||
let chunks_bitmap = bitvec![u8, Lsb0; 0; chunk_count as usize];
|
||||
|
||||
let mut etag_hash = [0u8; 32];
|
||||
let etag_digest = Sha256::digest(etag.as_bytes());
|
||||
etag_hash.copy_from_slice(&etag_digest[..]);
|
||||
|
||||
let mut url_hash = [0u8; 32];
|
||||
let url_digest = Sha256::digest(url.as_bytes());
|
||||
url_hash.copy_from_slice(&url_digest[..]);
|
||||
|
||||
Self {
|
||||
magic: *MAGIC_NUMBER,
|
||||
version: FORMAT_VERSION,
|
||||
file_size,
|
||||
etag: etag_hash,
|
||||
url_hash,
|
||||
chunk_size,
|
||||
chunk_count,
|
||||
chunks_bitmap,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_file<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
|
||||
writer.write_all(&self.magic)?;
|
||||
writer.write_all(&[self.version])?;
|
||||
writer.write_all(&self.file_size.to_le_bytes())?;
|
||||
writer.write_all(&self.etag)?;
|
||||
writer.write_all(&self.url_hash)?;
|
||||
writer.write_all(&self.chunk_size.to_le_bytes())?;
|
||||
writer.write_all(&self.chunk_count.to_le_bytes())?;
|
||||
|
||||
let bitmap_bytes = self.chunks_bitmap.as_raw_slice();
|
||||
writer.write_all(bitmap_bytes)?;
|
||||
|
||||
let header_size = 5 + 1 + 8 + 32 + 32 + 4 + 4 + bitmap_bytes.len();
|
||||
let padding_size = HEADER_SIZE - header_size;
|
||||
let padding = vec![0u8; padding_size];
|
||||
writer.write_all(&padding)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_from_file<R: Read + Seek>(reader: &mut R) -> Result<Self> {
|
||||
let mut magic = [0u8; 5];
|
||||
reader.read_exact(&mut magic)?;
|
||||
|
||||
if magic != *MAGIC_NUMBER {
|
||||
anyhow::bail!("Not a valid Hydra download file");
|
||||
}
|
||||
|
||||
let mut version = [0u8; 1];
|
||||
reader.read_exact(&mut version)?;
|
||||
|
||||
if version[0] != FORMAT_VERSION {
|
||||
anyhow::bail!("Incompatible format version");
|
||||
}
|
||||
|
||||
let mut file_size_bytes = [0u8; 8];
|
||||
reader.read_exact(&mut file_size_bytes)?;
|
||||
let file_size = u64::from_le_bytes(file_size_bytes);
|
||||
|
||||
let mut etag = [0u8; 32];
|
||||
reader.read_exact(&mut etag)?;
|
||||
|
||||
let mut url_hash = [0u8; 32];
|
||||
reader.read_exact(&mut url_hash)?;
|
||||
|
||||
let mut chunk_size_bytes = [0u8; 4];
|
||||
reader.read_exact(&mut chunk_size_bytes)?;
|
||||
let chunk_size = u32::from_le_bytes(chunk_size_bytes);
|
||||
|
||||
let mut chunk_count_bytes = [0u8; 4];
|
||||
reader.read_exact(&mut chunk_count_bytes)?;
|
||||
let chunk_count = u32::from_le_bytes(chunk_count_bytes);
|
||||
|
||||
let bitmap_bytes_len = (chunk_count as usize + 7) / 8;
|
||||
let mut bitmap_bytes = vec![0u8; bitmap_bytes_len];
|
||||
reader.read_exact(&mut bitmap_bytes)?;
|
||||
|
||||
let chunks_bitmap = BitVec::<u8, Lsb0>::from_vec(bitmap_bytes);
|
||||
|
||||
reader.seek(SeekFrom::Start(HEADER_SIZE as u64))?;
|
||||
|
||||
Ok(Self {
|
||||
magic,
|
||||
version: version[0],
|
||||
file_size,
|
||||
etag,
|
||||
url_hash,
|
||||
chunk_size,
|
||||
chunk_count,
|
||||
chunks_bitmap,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_chunk_complete(&mut self, chunk_index: usize) -> Result<()> {
|
||||
if chunk_index >= self.chunk_count as usize {
|
||||
anyhow::bail!("Chunk index out of bounds");
|
||||
}
|
||||
|
||||
self.chunks_bitmap.set(chunk_index, true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_chunk_complete(&self, chunk_index: usize) -> bool {
|
||||
if chunk_index >= self.chunk_count as usize {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.chunks_bitmap[chunk_index]
|
||||
}
|
||||
|
||||
fn get_incomplete_chunks(&self) -> Vec<(u64, u64)> {
|
||||
let incomplete_count = self.chunk_count as usize - self.chunks_bitmap.count_ones();
|
||||
let mut chunks = Vec::with_capacity(incomplete_count);
|
||||
let chunk_size = self.chunk_size as u64;
|
||||
|
||||
for i in 0..self.chunk_count as usize {
|
||||
if !self.is_chunk_complete(i) {
|
||||
let start = i as u64 * chunk_size;
|
||||
let end = std::cmp::min((i as u64 + 1) * chunk_size - 1, self.file_size - 1);
|
||||
chunks.push((start, end));
|
||||
}
|
||||
}
|
||||
|
||||
chunks
|
||||
}
|
||||
|
||||
fn is_download_complete(&self) -> bool {
|
||||
self.chunks_bitmap.count_ones() == self.chunk_count as usize
|
||||
}
|
||||
}
|
||||
|
||||
struct ProgressTracker {
|
||||
bar: Option<ProgressBar>,
|
||||
}
|
||||
|
||||
impl ProgressTracker {
|
||||
fn new(file_size: u64, silent: bool, enable_stats: bool) -> Result<Self> {
|
||||
let bar = if !silent || enable_stats {
|
||||
let pb = ProgressBar::new(file_size);
|
||||
pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("[{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")?
|
||||
);
|
||||
if silent {
|
||||
pb.set_draw_target(indicatif::ProgressDrawTarget::hidden());
|
||||
}
|
||||
Some(pb)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self { bar })
|
||||
}
|
||||
|
||||
fn increment(&self, amount: u64) {
|
||||
if let Some(pb) = &self.bar {
|
||||
pb.inc(amount);
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&self) {
|
||||
if let Some(pb) = &self.bar {
|
||||
pb.finish_with_message("Download complete");
|
||||
}
|
||||
}
|
||||
|
||||
fn get_stats(&self) -> Option<DownloadStats> {
|
||||
if let Some(pb) = &self.bar {
|
||||
let position = pb.position();
|
||||
let total = pb.length().unwrap_or(1);
|
||||
|
||||
Some(DownloadStats {
|
||||
progress_percent: position as f64 / total as f64,
|
||||
bytes_downloaded: position,
|
||||
total_size: total,
|
||||
speed_bytes_per_sec: pb.per_sec(),
|
||||
eta_seconds: pb.eta().as_secs(),
|
||||
elapsed_seconds: pb.elapsed().as_secs(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Downloader {
|
||||
client: Client,
|
||||
config: DownloadConfig,
|
||||
}
|
||||
|
||||
impl Downloader {
|
||||
async fn download(&self) -> Result<()> {
|
||||
let (file_size, filename, etag) = self.get_file_info().await?;
|
||||
let output_path = self.determine_output_path(filename);
|
||||
|
||||
if self.config.should_log() {
|
||||
println!("Detected filename: {}", output_path);
|
||||
}
|
||||
|
||||
let resume_manager = ResumeManager::try_from_file(
|
||||
&output_path,
|
||||
file_size,
|
||||
&etag,
|
||||
&self.config.url,
|
||||
self.config.chunk_size as u32,
|
||||
self.config.force_new,
|
||||
self.config.resume_only,
|
||||
)?;
|
||||
|
||||
let file = self.prepare_output_file(&output_path, file_size)?;
|
||||
let progress = ProgressTracker::new(file_size, self.config.silent, self.config.log)?;
|
||||
|
||||
let chunks = if resume_manager.is_download_complete() {
|
||||
if self.config.should_log() {
|
||||
println!("File is already fully downloaded, finalizing...");
|
||||
}
|
||||
resume_manager.finalize_download()?;
|
||||
return Ok(());
|
||||
} else {
|
||||
let completed_chunks = resume_manager.header.chunks_bitmap.count_ones() as u32;
|
||||
let total_chunks = resume_manager.header.chunk_count;
|
||||
|
||||
if completed_chunks > 0 {
|
||||
if self.config.should_log() {
|
||||
let percent_done = (completed_chunks as f64 / total_chunks as f64) * 100.0;
|
||||
println!("Resuming download: {:.1}% already downloaded", percent_done);
|
||||
}
|
||||
|
||||
if let Some(pb) = &progress.bar {
|
||||
let downloaded = file_size * completed_chunks as u64 / total_chunks as u64;
|
||||
pb.set_position(downloaded);
|
||||
pb.reset_elapsed();
|
||||
}
|
||||
}
|
||||
|
||||
resume_manager.get_incomplete_chunks()
|
||||
};
|
||||
|
||||
if self.config.should_log() {
|
||||
println!(
|
||||
"Downloading {} chunks of total {}",
|
||||
chunks.len(),
|
||||
resume_manager.header.chunk_count
|
||||
);
|
||||
}
|
||||
|
||||
self.process_chunks_with_resume(
|
||||
chunks,
|
||||
file,
|
||||
file_size,
|
||||
progress,
|
||||
output_path.clone(),
|
||||
resume_manager,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn determine_output_path(&self, filename: Option<String>) -> String {
|
||||
if Path::new(&self.config.output_path)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
== DEFAULT_OUTPUT_FILENAME
|
||||
&& filename.is_some()
|
||||
{
|
||||
filename.unwrap()
|
||||
} else {
|
||||
self.config.output_path.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_output_file(&self, path: &str, size: u64) -> Result<Arc<Mutex<BufWriter<File>>>> {
|
||||
let file = if Path::new(path).exists() {
|
||||
OpenOptions::new().read(true).write(true).open(path)?
|
||||
} else {
|
||||
let file = File::create(path)?;
|
||||
file.set_len(HEADER_SIZE as u64 + size)?;
|
||||
file
|
||||
};
|
||||
|
||||
Ok(Arc::new(Mutex::new(BufWriter::with_capacity(
|
||||
self.config.buffer_size,
|
||||
file,
|
||||
))))
|
||||
}
|
||||
|
||||
async fn process_chunks_with_resume(
|
||||
&self,
|
||||
chunks: Vec<(u64, u64)>,
|
||||
file: Arc<Mutex<BufWriter<File>>>,
|
||||
_file_size: u64,
|
||||
progress: ProgressTracker,
|
||||
real_filename: String,
|
||||
resume_manager: ResumeManager,
|
||||
) -> Result<()> {
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
let log_progress = if self.config.should_log_stats() {
|
||||
let progress_clone = progress.bar.clone();
|
||||
let filename = real_filename.clone();
|
||||
|
||||
let (log_cancel_tx, mut log_cancel_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
let log_task = tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1));
|
||||
let tracker = ProgressTracker {
|
||||
bar: progress_clone,
|
||||
};
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
if let Some(stats) = tracker.get_stats() {
|
||||
let json_output = json!({
|
||||
"progress": stats.progress_percent,
|
||||
"speed_bps": stats.speed_bytes_per_sec,
|
||||
"downloaded_bytes": stats.bytes_downloaded,
|
||||
"total_bytes": stats.total_size,
|
||||
"eta_seconds": stats.eta_seconds,
|
||||
"elapsed_seconds": stats.elapsed_seconds,
|
||||
"filename": filename
|
||||
});
|
||||
println!("{}", json_output);
|
||||
}
|
||||
}
|
||||
_ = &mut log_cancel_rx => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Some((log_task, log_cancel_tx))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let resume_manager = Arc::new(Mutex::new(resume_manager));
|
||||
|
||||
for (start, end) in chunks {
|
||||
let client = self.client.clone();
|
||||
let url = self.config.url.clone();
|
||||
let file_clone = Arc::clone(&file);
|
||||
let pb_clone = progress.bar.clone();
|
||||
let manager_clone = Arc::clone(&resume_manager);
|
||||
let headers = self.config.headers.clone();
|
||||
let force_download = self.config.force_download;
|
||||
let should_log = self.config.should_log();
|
||||
|
||||
let chunk_size = self.config.chunk_size as u64;
|
||||
let chunk_index = (start / chunk_size) as usize;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let result = Self::download_chunk_with_retry(
|
||||
client,
|
||||
url,
|
||||
start,
|
||||
end,
|
||||
file_clone,
|
||||
pb_clone,
|
||||
DEFAULT_MAX_RETRIES,
|
||||
&headers,
|
||||
force_download,
|
||||
should_log,
|
||||
)
|
||||
.await;
|
||||
|
||||
if result.is_ok() {
|
||||
let mut manager = manager_clone.lock().await;
|
||||
manager.set_chunk_complete(chunk_index)?;
|
||||
}
|
||||
|
||||
result
|
||||
}));
|
||||
|
||||
if tasks.len() >= self.config.num_connections {
|
||||
if let Some(result) = tasks.next().await {
|
||||
result??;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(result) = tasks.next().await {
|
||||
result??;
|
||||
}
|
||||
|
||||
{
|
||||
let mut writer = file.lock().await;
|
||||
writer.flush()?;
|
||||
}
|
||||
|
||||
progress.finish();
|
||||
|
||||
if let Some((log_handle, log_cancel_tx)) = log_progress {
|
||||
let _ = log_cancel_tx.send(());
|
||||
let _ = log_handle.await;
|
||||
}
|
||||
|
||||
let manager = resume_manager.lock().await;
|
||||
if manager.is_download_complete() {
|
||||
if self.config.should_log() {
|
||||
println!("Download complete, finalizing file...");
|
||||
}
|
||||
manager.finalize_download()?;
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
|
||||
if self.config.should_log_stats() {
|
||||
let json_output = json!({
|
||||
"progress": 1.0,
|
||||
"speed_bps": 0.0,
|
||||
"downloaded_bytes": _file_size,
|
||||
"total_bytes": _file_size,
|
||||
"eta_seconds": 0,
|
||||
"elapsed_seconds": if let Some(pb) = &progress.bar { pb.elapsed().as_secs() } else { 0 },
|
||||
"filename": real_filename
|
||||
});
|
||||
println!("{}", json_output);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_chunk_with_retry(
|
||||
client: Client,
|
||||
url: String,
|
||||
start: u64,
|
||||
end: u64,
|
||||
file: Arc<Mutex<BufWriter<File>>>,
|
||||
progress_bar: Option<ProgressBar>,
|
||||
max_retries: usize,
|
||||
headers: &[String],
|
||||
force_download: bool,
|
||||
should_log: bool,
|
||||
) -> Result<()> {
|
||||
let mut retries = 0;
|
||||
loop {
|
||||
match Self::download_chunk(
|
||||
client.clone(),
|
||||
url.clone(),
|
||||
start,
|
||||
end,
|
||||
file.clone(),
|
||||
progress_bar.clone(),
|
||||
headers,
|
||||
force_download,
|
||||
should_log,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => return Ok(()),
|
||||
Err(e) => {
|
||||
retries += 1;
|
||||
if retries >= max_retries {
|
||||
return Err(e);
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(
|
||||
RETRY_BACKOFF_MS * (2_u64.pow(retries as u32 - 1)),
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_chunk(
|
||||
client: Client,
|
||||
url: String,
|
||||
start: u64,
|
||||
end: u64,
|
||||
file: Arc<Mutex<BufWriter<File>>>,
|
||||
progress_bar: Option<ProgressBar>,
|
||||
headers: &[String],
|
||||
force_download: bool,
|
||||
should_log: bool,
|
||||
) -> Result<()> {
|
||||
let mut req = client
|
||||
.get(&url)
|
||||
.header("Range", format!("bytes={}-{}", start, end));
|
||||
|
||||
for header in headers {
|
||||
if let Some(idx) = header.find(':') {
|
||||
let (name, value) = header.split_at(idx);
|
||||
let value = value[1..].trim();
|
||||
req = req.header(name.trim(), value);
|
||||
}
|
||||
}
|
||||
|
||||
let resp = req.send().await?;
|
||||
|
||||
if resp.status() != StatusCode::PARTIAL_CONTENT && resp.status() != StatusCode::OK {
|
||||
if !force_download {
|
||||
anyhow::bail!("Server does not support Range requests");
|
||||
} else if should_log {
|
||||
println!("Server does not support Range requests, ignoring...");
|
||||
}
|
||||
}
|
||||
|
||||
let mut stream = resp.bytes_stream();
|
||||
let mut position = start;
|
||||
let mut total_bytes = 0;
|
||||
let expected_bytes = end - start + 1;
|
||||
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
let chunk = chunk_result?;
|
||||
let chunk_size = chunk.len() as u64;
|
||||
|
||||
total_bytes += chunk_size;
|
||||
if total_bytes > expected_bytes {
|
||||
let remaining = expected_bytes - (total_bytes - chunk_size);
|
||||
let mut writer = file.lock().await;
|
||||
writer.seek(SeekFrom::Start(HEADER_SIZE as u64 + position))?;
|
||||
writer.write_all(&chunk[..remaining as usize])?;
|
||||
|
||||
let tracker = ProgressTracker {
|
||||
bar: progress_bar.clone(),
|
||||
};
|
||||
tracker.increment(remaining);
|
||||
break;
|
||||
}
|
||||
|
||||
let mut writer = file.lock().await;
|
||||
writer.seek(SeekFrom::Start(HEADER_SIZE as u64 + position))?;
|
||||
writer.write_all(&chunk)?;
|
||||
drop(writer);
|
||||
|
||||
position += chunk_size;
|
||||
let tracker = ProgressTracker {
|
||||
bar: progress_bar.clone(),
|
||||
};
|
||||
tracker.increment(chunk_size);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_file_info(&self) -> Result<(u64, Option<String>, String)> {
|
||||
let mut req = self.client.head(&self.config.url);
|
||||
|
||||
for header in &self.config.headers {
|
||||
if let Some(idx) = header.find(':') {
|
||||
let (name, value) = header.split_at(idx);
|
||||
let value = value[1..].trim();
|
||||
req = req.header(name.trim(), value);
|
||||
}
|
||||
}
|
||||
|
||||
let resp = req.send().await?;
|
||||
|
||||
let accepts_ranges = resp
|
||||
.headers()
|
||||
.get("accept-ranges")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|v| v.contains("bytes"))
|
||||
.unwrap_or(false);
|
||||
|
||||
if !accepts_ranges {
|
||||
let range_check = self
|
||||
.client
|
||||
.get(&self.config.url)
|
||||
.header("Range", "bytes=0-0")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if range_check.status() != StatusCode::PARTIAL_CONTENT {
|
||||
if !self.config.force_download {
|
||||
anyhow::bail!(
|
||||
"Server does not support Range requests, cannot continue with parallel download"
|
||||
);
|
||||
} else if self.config.should_log() {
|
||||
println!("Server does not support Range requests, ignoring...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let file_size = if let Some(content_length) = resp.headers().get("content-length") {
|
||||
content_length.to_str()?.parse()?
|
||||
} else {
|
||||
anyhow::bail!("Could not determine file size")
|
||||
};
|
||||
|
||||
let etag = if let Some(etag_header) = resp.headers().get("etag") {
|
||||
etag_header.to_str()?.to_string()
|
||||
} else {
|
||||
format!(
|
||||
"no-etag-{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
)
|
||||
};
|
||||
|
||||
let filename = self.extract_filename_from_response(&resp);
|
||||
|
||||
Ok((file_size, filename, etag))
|
||||
}
|
||||
|
||||
fn extract_filename_from_response(&self, resp: &reqwest::Response) -> Option<String> {
|
||||
if let Some(disposition) = resp.headers().get("content-disposition") {
|
||||
if let Ok(disposition_str) = disposition.to_str() {
|
||||
if let Some(filename) = Self::parse_content_disposition(disposition_str) {
|
||||
return Some(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self::extract_filename_from_url(&self.config.url)
|
||||
}
|
||||
|
||||
fn parse_content_disposition(disposition: &str) -> Option<String> {
|
||||
if let Some(idx) = disposition.find("filename=") {
|
||||
let start = idx + 9;
|
||||
let mut end = disposition.len();
|
||||
|
||||
if disposition.as_bytes().get(start) == Some(&b'"') {
|
||||
let quoted_name = &disposition[start + 1..];
|
||||
if let Some(quote_end) = quoted_name.find('"') {
|
||||
return Some(quoted_name[..quote_end].to_string());
|
||||
}
|
||||
} else {
|
||||
if let Some(semicolon) = disposition[start..].find(';') {
|
||||
end = start + semicolon;
|
||||
}
|
||||
return Some(disposition[start..end].to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn extract_filename_from_url(url: &str) -> Option<String> {
|
||||
if let Ok(parsed_url) = Url::parse(url) {
|
||||
let path = parsed_url.path();
|
||||
if let Some(path_filename) = Path::new(path).file_name() {
|
||||
if let Some(filename_str) = path_filename.to_str() {
|
||||
if !filename_str.is_empty() {
|
||||
if let Ok(decoded) = urlencoding::decode(filename_str) {
|
||||
return Some(decoded.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct ResumeManager {
|
||||
header: HydraHeader,
|
||||
file_path: String,
|
||||
}
|
||||
|
||||
impl ResumeManager {
|
||||
fn try_from_file(
|
||||
path: &str,
|
||||
file_size: u64,
|
||||
etag: &str,
|
||||
url: &str,
|
||||
chunk_size: u32,
|
||||
force_new: bool,
|
||||
resume_only: bool,
|
||||
) -> Result<Self> {
|
||||
if force_new {
|
||||
if Path::new(path).exists() {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
|
||||
return Self::create_new_file(path, file_size, etag, url, chunk_size);
|
||||
}
|
||||
|
||||
if let Ok(file) = File::open(path) {
|
||||
let mut reader = BufReader::new(file);
|
||||
match HydraHeader::read_from_file(&mut reader) {
|
||||
Ok(header) => {
|
||||
let current_url_hash = Sha256::digest(url.as_bytes());
|
||||
|
||||
let url_matches = header.url_hash == current_url_hash.as_slice();
|
||||
let size_matches = header.file_size == file_size;
|
||||
|
||||
if url_matches && size_matches {
|
||||
return Ok(Self {
|
||||
header,
|
||||
file_path: path.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if resume_only {
|
||||
anyhow::bail!(
|
||||
"Existing file is not compatible and resume_only option is active"
|
||||
);
|
||||
}
|
||||
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
Err(e) => {
|
||||
if resume_only {
|
||||
return Err(anyhow::anyhow!("Could not read file to resume: {}", e));
|
||||
}
|
||||
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
} else if resume_only {
|
||||
anyhow::bail!("File not found and resume_only option is active");
|
||||
}
|
||||
|
||||
Self::create_new_file(path, file_size, etag, url, chunk_size)
|
||||
}
|
||||
|
||||
fn create_new_file(
|
||||
path: &str,
|
||||
file_size: u64,
|
||||
etag: &str,
|
||||
url: &str,
|
||||
chunk_size: u32,
|
||||
) -> Result<Self> {
|
||||
let header = HydraHeader::new(file_size, etag, url, chunk_size);
|
||||
let file = File::create(path)?;
|
||||
file.set_len(HEADER_SIZE as u64 + file_size)?;
|
||||
|
||||
let mut writer = BufWriter::new(file);
|
||||
header.write_to_file(&mut writer)?;
|
||||
writer.flush()?;
|
||||
|
||||
Ok(Self {
|
||||
header,
|
||||
file_path: path.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_incomplete_chunks(&self) -> Vec<(u64, u64)> {
|
||||
self.header.get_incomplete_chunks()
|
||||
}
|
||||
|
||||
fn set_chunk_complete(&mut self, chunk_index: usize) -> Result<()> {
|
||||
self.header.set_chunk_complete(chunk_index)?;
|
||||
|
||||
let file = OpenOptions::new().write(true).open(&self.file_path)?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
let bitmap_offset = 5 + 1 + 8 + 32 + 32 + 4 + 4;
|
||||
writer.seek(SeekFrom::Start(bitmap_offset as u64))?;
|
||||
|
||||
let bitmap_bytes = self.header.chunks_bitmap.as_raw_slice();
|
||||
writer.write_all(bitmap_bytes)?;
|
||||
writer.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_download_complete(&self) -> bool {
|
||||
self.header.is_download_complete()
|
||||
}
|
||||
|
||||
fn finalize_download(&self) -> Result<()> {
|
||||
if !self.is_download_complete() {
|
||||
anyhow::bail!("Download is not complete");
|
||||
}
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&self.file_path)?;
|
||||
|
||||
let file_size = self.header.file_size;
|
||||
|
||||
let buffer_size = 64 * 1024 * 1024;
|
||||
let mut buffer = vec![0u8; buffer_size.min(file_size as usize)];
|
||||
|
||||
let mut file = BufReader::new(file);
|
||||
let mut write_pos = 0;
|
||||
let mut read_pos = HEADER_SIZE as u64;
|
||||
|
||||
while read_pos < (HEADER_SIZE as u64 + file_size) {
|
||||
file.seek(SeekFrom::Start(read_pos))?;
|
||||
|
||||
let bytes_read = file.read(&mut buffer)?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
file.get_mut().seek(SeekFrom::Start(write_pos))?;
|
||||
|
||||
file.get_mut().write_all(&buffer[..bytes_read])?;
|
||||
|
||||
read_pos += bytes_read as u64;
|
||||
write_pos += bytes_read as u64;
|
||||
}
|
||||
|
||||
file.get_mut().set_len(file_size)?;
|
||||
file.get_mut().flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = CliArgs::parse();
|
||||
|
||||
let config = DownloadConfig {
|
||||
url: args.url.clone(),
|
||||
output_path: args.output,
|
||||
num_connections: args.connections,
|
||||
chunk_size: args.chunk_size * 1024 * 1024,
|
||||
buffer_size: args.buffer_size * 1024 * 1024,
|
||||
verbose: args.verbose,
|
||||
silent: args.silent,
|
||||
log: args.log,
|
||||
force_new: args.force_new,
|
||||
resume_only: args.resume_only,
|
||||
headers: args.header,
|
||||
force_download: args.force_download,
|
||||
};
|
||||
|
||||
if config.force_new && config.resume_only {
|
||||
eprintln!("Error: --force-new and --resume-only options cannot be used together");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let downloader = Downloader {
|
||||
client: Client::new(),
|
||||
config,
|
||||
};
|
||||
|
||||
if downloader.config.should_log() {
|
||||
println!(
|
||||
"Starting download with {} connections, chunk size: {}MB, buffer: {}MB",
|
||||
downloader.config.num_connections, args.chunk_size, args.buffer_size
|
||||
);
|
||||
println!("URL: {}", args.url);
|
||||
|
||||
if downloader.config.force_new {
|
||||
println!("Forcing new download, ignoring existing files");
|
||||
} else if downloader.config.resume_only {
|
||||
println!("Only resuming existing download");
|
||||
} else {
|
||||
println!("Resuming download if possible");
|
||||
}
|
||||
}
|
||||
|
||||
downloader.download().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,7 +2,6 @@ const { default: axios } = require("axios");
|
||||
const util = require("node:util");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
|
||||
const exec = util.promisify(require("node:child_process").exec);
|
||||
|
||||
@@ -47,79 +46,11 @@ const downloadLudusavi = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const downloadAria2WindowsAndLinux = async () => {
|
||||
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.mkdirSync("aria2");
|
||||
fs.copyFileSync(
|
||||
path.join(file.replace(".zip", ""), "aria2c.exe"),
|
||||
"aria2/aria2c.exe"
|
||||
);
|
||||
fs.rmSync(file.replace(".zip", ""), { recursive: true });
|
||||
} 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);
|
||||
});
|
||||
};
|
||||
|
||||
const copyAria2Macos = async () => {
|
||||
console.log("Checking if aria2 is installed...");
|
||||
|
||||
const isAria2Installed = spawnSync("which", ["aria2c"]).status;
|
||||
|
||||
if (isAria2Installed != 0) {
|
||||
console.log("Please install aria2");
|
||||
console.log("brew install aria2");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Copying aria2 binary...");
|
||||
fs.mkdirSync("aria2");
|
||||
await exec(`cp $(which aria2c) aria2/aria2c`);
|
||||
};
|
||||
|
||||
const copyAria2 = () => {
|
||||
const aria2Path =
|
||||
process.platform === "win32" ? "aria2/aria2c.exe" : "aria2/aria2c";
|
||||
|
||||
if (fs.existsSync(aria2Path)) {
|
||||
console.log("Aria2 already exists, skipping download...");
|
||||
return;
|
||||
}
|
||||
if (process.platform == "darwin") {
|
||||
copyAria2Macos();
|
||||
} else {
|
||||
downloadAria2WindowsAndLinux();
|
||||
}
|
||||
};
|
||||
|
||||
copyAria2();
|
||||
downloadLudusavi();
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
const binariesPath = path.join(__dirname, "..", "binaries");
|
||||
|
||||
fs.chmodSync(path.join(binariesPath, "7zz"), 0o755);
|
||||
fs.chmodSync(path.join(binariesPath, "7zzs"), 0o755);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -255,6 +255,12 @@
|
||||
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL للمصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
||||
"validate_download_source": "تحقق",
|
||||
"remove_download_source": "إزالة",
|
||||
"removed_download_sources": "تمت إزالة مصادر التنزيل",
|
||||
"cancel_button_confirmation_delete_all_sources": "لا",
|
||||
"confirm_button_confirmation_delete_all_sources": "نعم، احذف كل شيء",
|
||||
"description_confirmation_delete_all_sources": "سوف تقوم بحذف جميع مصادر التنزيل",
|
||||
"title_confirmation_delete_all_sources": "احذف جميع مصادر التنزيل",
|
||||
"button_delete_all_sources": "قم بإزالة جميع مصادر التنزيل",
|
||||
"add_download_source": "إضافة مصدر",
|
||||
"download_count_zero": "لا توجد خيارات تنزيل",
|
||||
"download_count_one": "{{countFormatted}} خيار تنزيل",
|
||||
|
||||
@@ -107,6 +107,12 @@
|
||||
"notifications": "Апавяшчэнні",
|
||||
"enable_download_notifications": "Па сканчэнні сцягванні",
|
||||
"enable_repack_list_notifications": "Пры даданні новага рэпака",
|
||||
"cancel_button_confirmation_delete_all_sources": "Няма",
|
||||
"confirm_button_confirmation_delete_all_sources": "Так,выдаліць усё",
|
||||
"description_confirmation_delete_all_sources": "Вы выдаліце ўсе крыніцы сцягвання",
|
||||
"title_confirmation_delete_all_sources": "Выдаліць усе крыніцы сцягвання",
|
||||
"removed_download_sources": "Крыніцы сцягвання выдалены",
|
||||
"button_delete_all_sources": "Премахнете всички източници на изтегляне",
|
||||
"behavior": "Паводзіны",
|
||||
"quit_app_instead_hiding": "Закрываць праграму замест таго, каб хаваць яе ў трэй",
|
||||
"launch_with_system": "Запускаць праграму пры запуску сыстэмы"
|
||||
|
||||
@@ -253,6 +253,12 @@
|
||||
"download_source_errored": "Сгрешен",
|
||||
"sync_download_sources": "Синхронизирай източниците",
|
||||
"removed_download_source": "Източника за сваляне е премахнат",
|
||||
"cancel_button_confirmation_delete_all_sources": "не",
|
||||
"confirm_button_confirmation_delete_all_sources": "Да, удалить все",
|
||||
"description_confirmation_delete_all_sources": "Вы удалите все источники загрузки",
|
||||
"title_confirmation_delete_all_sources": "Удалить все источники загрузки",
|
||||
"removed_download_sources": "Шрифты удалены",
|
||||
"button_delete_all_sources": "Удалить все источники загрузки",
|
||||
"added_download_source": "Добавен източник за сваляне",
|
||||
"download_sources_synced": "Всички източници за сваляне са синхронизирани",
|
||||
"insert_valid_json_url": "Добавете ваиден JSON линк",
|
||||
|
||||
@@ -175,6 +175,12 @@
|
||||
"download_sources_description": "Hydra buscarà els enllaços de descàrrega d'aquestes fonts. L'URL d'origen ha de ser un enllaç directe a un fitxer .json que contingui els enllaços de descàrrega.",
|
||||
"validate_download_source": "Valida",
|
||||
"remove_download_source": "Elimina",
|
||||
"cancel_button_confirmation_delete_all_sources": "No",
|
||||
"confirm_button_confirmation_delete_all_sources": "Sí, esborra-ho tot",
|
||||
"description_confirmation_delete_all_sources": "Eliminareu totes les fonts de descàrrega",
|
||||
"title_confirmation_delete_all_sources": "Suprimeix totes les fonts de baixada",
|
||||
"removed_download_sources": "S'han eliminat les fonts de descàrrega",
|
||||
"button_delete_all_sources": "Elimina totes les fonts de baixada",
|
||||
"add_download_source": "Afegeix font",
|
||||
"download_count_zero": "No hi ha baixades a la llista",
|
||||
"download_count_one": "{{countFormatted}} a la llista de baixades",
|
||||
|
||||
@@ -44,11 +44,21 @@
|
||||
"downloading_metadata": "Stahuji metadata: {{title}}…",
|
||||
"downloading": "Stahuji {{title}}… ({{percentage}} staženo) - Odhadovaný čas {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Stahuji {{title}}… ({{percentage}} staženo) - Počítám zbývající čas…",
|
||||
"checking_files": "Kontroluji soubory: {{title}}… ({{percentage}} ověřeno)"
|
||||
"checking_files": "Kontroluji soubory: {{title}}… ({{percentage}} ověřeno)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "Instalace dokončena",
|
||||
"installation_complete_message": "Běžné redistribuovatelné komponenty byly úspěšně nainstalovány"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Další strana",
|
||||
"previous_page": "Předchozí strana"
|
||||
"search": "Filtr…",
|
||||
"developers": "Vývojáři",
|
||||
"genres": "Žánry",
|
||||
"tags": "Tagy",
|
||||
"publishers": "Vydavatelé",
|
||||
"download_sources": "Zdroje stahování",
|
||||
"result_count": "Výsledky: {{resultCount}}",
|
||||
"filter_count": "Dostupné: {{filterCount}}",
|
||||
"clear_filters": "Vyčistit vybrané filtry: {{filterCount}}"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Otevřít možnosti stahování",
|
||||
@@ -107,6 +117,7 @@
|
||||
"open_download_location": "Zobrazit stažené soubory",
|
||||
"create_shortcut": "Vytvořit zástupce na ploše",
|
||||
"remove_files": "Odebrat soubory",
|
||||
"clear": "Vyčistit",
|
||||
"remove_from_library_title": "Jste si jisti?",
|
||||
"remove_from_library_description": "Tohle odstraní {{game}} z vaší knihovny",
|
||||
"options": "Možnosti",
|
||||
@@ -160,6 +171,9 @@
|
||||
"loading_save_preview": "Hledání uložených her...",
|
||||
"wine_prefix": "Wine Prefix",
|
||||
"wine_prefix_description": "Wine Prefix použit pro spuštění této hry",
|
||||
"launch_options": "Možnosti spuštění",
|
||||
"launch_options_description": "Pokročilí uživatelé mohou zadat speciální parametry spuštění (experimentální funkce)",
|
||||
"launch_options_placeholder": "Žádné parametery nebyly specifikovány",
|
||||
"no_download_option_info": "Žádné informace nejsou dostupny",
|
||||
"backup_deletion_failed": "Nepovedlo se odstranit zálohu",
|
||||
"max_number_of_artifacts_reached": "Dosáhli jste maximálního počtu záloh pro tuto hru",
|
||||
@@ -167,7 +181,23 @@
|
||||
"manage_files_description": "Spravovat, které soubory budou zálohovány a obnoveny",
|
||||
"select_folder": "Vybrat složku",
|
||||
"backup_from": "Zálohy z {{date}}",
|
||||
"custom_backup_location_set": "Vlastní umístění záloh nastaveno"
|
||||
"custom_backup_location_set": "Vlastní umístění záloh nastaveno",
|
||||
"automatic_backup_from": "Automatická záloha z {{date}}",
|
||||
"enable_automatic_cloud_sync": "Povolit automatické zálohy v cloudu",
|
||||
"no_directory_selected": "Žádná složka není zvolena",
|
||||
"no_write_permission": "Nemohu stahovat do této složky. Klikni sem pro více informací.",
|
||||
"reset_achievements": "Resetovat achievementy",
|
||||
"reset_achievements_description": "Toto zresetuje všechny achievementy pro hru {{game}}",
|
||||
"reset_achievements_title": "Jste si jisti?",
|
||||
"reset_achievements_success": "Achievementy úspěšně resetovány",
|
||||
"reset_achievements_error": "Nepodařilo se resetovat achievementy",
|
||||
"download_error_gofile_quota_exceeded": "Překročili jste vaši měsíční GoFile kvótu. Prosím vyčkejte na resetování kvóty.",
|
||||
"download_error_real_debrid_account_not_authorized": "Váš Real-Debrid účet není autorizován pro vytváření nových stahování. Prosím zkontrolujte nastavení vašeho účtu a zkuste to znovu.",
|
||||
"download_error_not_cached_in_real_debrid": "Toto stahování není dostupné na Real-Debrid a získávání informací o stahování z Real-Debrid není zatím dostupné.",
|
||||
"download_error_not_cached_in_torbox": "Toto stahování není dostupné na Torbox a získávání informací o stahování z Torbox není zatím dostupné.",
|
||||
"game_removed_from_favorites": "Hra odebrána z oblíbených",
|
||||
"game_added_to_favorites": "Hra přidána do oblíbených",
|
||||
"automatically_extract_downloaded_files": "Automaticky rozbalit stažené soubory"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Aktivovat hydru",
|
||||
@@ -200,7 +230,13 @@
|
||||
"queued": "V řadě",
|
||||
"no_downloads_title": "Prázdno..",
|
||||
"no_downloads_description": "Ještě jsi zatím nic nestáhl přes Hydru, ale furt není pozdě začít.",
|
||||
"checking_files": "Kontroluji soubory…"
|
||||
"checking_files": "Kontroluji soubory…",
|
||||
"seeding": "Seedování",
|
||||
"stop_seeding": "Zastavování seedování",
|
||||
"resume_seeding": "Obnovit seedování",
|
||||
"options": "Spravovat",
|
||||
"extract": "Rozbalit soubory",
|
||||
"extracting": "Rozbalování souborů…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Umístění stahování",
|
||||
@@ -237,6 +273,12 @@
|
||||
"download_source_errored": "Chyba",
|
||||
"sync_download_sources": "Synchronizovat zdroje",
|
||||
"removed_download_source": "Zdroj odebrán",
|
||||
"cancel_button_confirmation_delete_all_sources": "Žádný",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ano, smazat vše",
|
||||
"description_confirmation_delete_all_sources": "Smažete všechny zdroje stahování",
|
||||
"title_confirmation_delete_all_sources": "Odstraňte všechny zdroje stahování",
|
||||
"removed_download_sources": "Zdroje stahování byly odstraněny",
|
||||
"button_delete_all_sources": "Odstraňte všechny zdroje stahování",
|
||||
"added_download_source": "Zdroj přidán",
|
||||
"download_sources_synced": "Všechny zdroje jsou synchronizovány",
|
||||
"insert_valid_json_url": "Zadej platnou JSON adresu",
|
||||
@@ -255,9 +297,65 @@
|
||||
"must_be_valid_url": "Zdroj musí být platký odkaz URL",
|
||||
"blocked_users": "Zablokovaní uživatelé",
|
||||
"user_unblocked": "Uživatel byl odblokován",
|
||||
"enable_achievement_notifications": "Když je odemknut achievement",
|
||||
"enable_achievement_notifications": "Když je odemčen achievement",
|
||||
"launch_minimized": "Spustit v minimalizovaném režimu",
|
||||
"disable_nsfw_alert": "Deaktivovat upozornění na nevhodný obsah"
|
||||
"disable_nsfw_alert": "Deaktivovat upozornění na nevhodný obsah",
|
||||
"seed_after_download_complete": "Seedovat až skončí stahování",
|
||||
"show_hidden_achievement_description": "Zobrazit popisy skrytých achievementů dříve, než jsou odemčeny",
|
||||
"account": "Účet",
|
||||
"no_users_blocked": "Nemáte žádného uživatele zablokovaného",
|
||||
"subscription_active_until": "Vaše Hydra cloud předplatné je platné do {{date}}",
|
||||
"manage_subscription": "Spravovat předplatné",
|
||||
"update_email": "Změnit email",
|
||||
"update_password": "Změnit heslo",
|
||||
"current_email": "Současný email:",
|
||||
"no_email_account": "Zatím nemáte nastavený email",
|
||||
"account_data_updated_successfully": "Data vašeho účtu byly úspěšně upraveny",
|
||||
"renew_subscription": "Obnovit předplatné Hydra Cloud",
|
||||
"subscription_expired_at": "Vaše předplatné vypršelo {{date}}",
|
||||
"no_subscription": "Užijte si Hydru v nejlepší možné podobě",
|
||||
"become_subscriber": "Připojit se k předplatnému Hydra Cloud",
|
||||
"subscription_renew_cancelled": "Automatické obnovování je zrušenu",
|
||||
"subscription_renews_on": "Vaše předplatné se obnoví {{date}}",
|
||||
"bill_sent_until": "Vaše příští faktura bude odeslána nejpozději do tohoto dne",
|
||||
"no_themes": "Vypadá to že ještě nemáte žádné vzhledy, ale nebojte, klikněte sem pro vytvoření vašeho prvního mistrovského díla!",
|
||||
"editor_tab_code": "Kód",
|
||||
"editor_tab_info": "Info",
|
||||
"editor_tab_save": "Uložit",
|
||||
"web_store": "Webový obchod",
|
||||
"clear_themes": "Vyčistit",
|
||||
"create_theme": "Vytvořit",
|
||||
"create_theme_modal_title": "Vytvořit vlastní vzhled",
|
||||
"create_theme_modal_description": "Vytvořte si vlastní styl, abyste si mohli ozdobit Hydru",
|
||||
"theme_name": "Název",
|
||||
"insert_theme_name": "Vložte název vzhledu",
|
||||
"set_theme": "Nastavit vzhled",
|
||||
"unset_theme": "Zrušit vzhled",
|
||||
"delete_theme": "Odstranit vzhled",
|
||||
"edit_theme": "Upravit vzhled",
|
||||
"delete_all_themes": "Smazat všechny vzhledy",
|
||||
"delete_all_themes_description": "Toto smaže všechny vaše vzhledy",
|
||||
"delete_theme_description": "Toto smaže vzhled {{theme}}",
|
||||
"cancel": "Zrušit",
|
||||
"appearance": "Vzhled",
|
||||
"enable_torbox": "Povolit TorBox",
|
||||
"torbox_description": "TorBox je prémiový seedbox server který se vyrovná i těm nejlepším seedbox serverům na trhu.",
|
||||
"torbox_account_linked": "TorBox účet propojen",
|
||||
"create_real_debrid_account": "Klikni sem pokud ještě nemáš Real-Debrid účet",
|
||||
"create_torbox_account": "Klikni sem pokud ještě nemáš TorBox účet",
|
||||
"real_debrid_account_linked": "Real-Debrid účet propojen",
|
||||
"name_min_length": "Název vzhledu musí být minimálně 3 znaky dlouhý",
|
||||
"import_theme": "Vložit vzhled",
|
||||
"import_theme_description": "Chystáte se vložit vzhled {{theme}} z obchodu vzhledů",
|
||||
"error_importing_theme": "Nastala chyba při vkládání vzhledu",
|
||||
"theme_imported": "Vzhled úspěšně vložen",
|
||||
"enable_friend_request_notifications": "Při obdržení žádosti o přátelství",
|
||||
"enable_auto_install": "Automaticky stahovat aktualizace",
|
||||
"common_redist": "Běžné redistribuovatelné komponenty",
|
||||
"common_redist_description": "Běžné redistribuovatelné komponenty jsou potřeba pro spuštění určitých her. Je doporučeno je nainstalovat, aby se předešlo problémům.",
|
||||
"install_common_redist": "Nainstalovat",
|
||||
"installing_common_redist": "Instalování…",
|
||||
"show_download_speed_in_megabytes": "Ukázat rychlost stahování v megabytech"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Stahování dokončeno",
|
||||
@@ -267,14 +365,20 @@
|
||||
"repack_count_other": "{{count}} repacky přidány",
|
||||
"new_update_available": "Version {{version}} je dostupná",
|
||||
"restart_to_install_update": "Restartuj Hydru pro aktualizaci",
|
||||
"notification_achievement_unlocked_title": "Achievement pro {{game}} byl odemknut",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} a dalších {{count}} byly odemknuty"
|
||||
"notification_achievement_unlocked_title": "Achievement pro {{game}} byl odemčen",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} a dalších {{count}} byly odemčeny",
|
||||
"new_friend_request_description": "Obdrželi jste novou žádost o přátelství",
|
||||
"new_friend_request_title": "Nová žádost o přátelství",
|
||||
"extraction_complete": "Rozbalování dokončeno",
|
||||
"game_extracted": "{{title}} úspěšně rozbaleno"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Otevřít Hydru",
|
||||
"quit": "Odejít"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Dostupné",
|
||||
"available_other": "Dostupné",
|
||||
"no_downloads": "Žádné možnosti stahování nenalezeny"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
@@ -357,7 +461,17 @@
|
||||
"your_friend_code": "Tvůj kód přítele:",
|
||||
"upload_banner": "Nahrát banner profilu",
|
||||
"uploading_banner": "Nahrávání banneru",
|
||||
"background_image_updated": "Obrázek pozadí byl změněn"
|
||||
"background_image_updated": "Obrázek pozadí byl změněn",
|
||||
"stats": "Statistiky",
|
||||
"achievements": "Achievementy",
|
||||
"games": "Hry",
|
||||
"top_percentile": "Top {{percentile}}%",
|
||||
"ranking_updated_weekly": "Žebříčky jsou aktualizovány každý týden",
|
||||
"playing": "Hraje {{game}}",
|
||||
"achievements_unlocked": "Achievements odemčen",
|
||||
"earned_points": "Získané body",
|
||||
"show_achievements_on_profile": "Zobrazit vaše odemčené achievementy na profilu",
|
||||
"show_points_on_profile": "Zobrazit vaše získané body na profilu"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Achievement odemčen",
|
||||
@@ -367,7 +481,12 @@
|
||||
"subscription_needed": "Je vyžadováno předplatné Hydra Cloud pro zobrazení tohoto obsahu",
|
||||
"new_achievements_unlocked": "Odemčeno {{achievementCount}} nových achievementů z {{gameCount}} her",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievementů",
|
||||
"achievements_unlocked_for_game": "Odemčeno {{achievementCount}} nových achievementů pro {{gameTitle}}"
|
||||
"achievements_unlocked_for_game": "Odemčeno {{achievementCount}} nových achievementů pro {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "Toho je skrytý achievement",
|
||||
"achievement_earn_points": "Získej {{points}} bodů tímto achievementem",
|
||||
"earned_points": "Získané body",
|
||||
"available_points": "Dostupné body:",
|
||||
"how_to_earn_achievements_points": "Jak získat body achievementů?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Předplatné Hydra Cloud",
|
||||
@@ -377,6 +496,10 @@
|
||||
"animated_profile_picture": "Animované profilové obrázky",
|
||||
"premium_support": "Prémiová podpora",
|
||||
"show_and_compare_achievements": "Zobraz a porovnej achievementy s ostatními uživateli",
|
||||
"animated_profile_banner": "Animovaný banner na profilu"
|
||||
"animated_profile_banner": "Animovaný banner na profilu",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Právě jste objevili funkci předplatného Hydra Cloud!",
|
||||
"learn_more": "Zjistit více",
|
||||
"debrid_description": "Stahovat až 4x rychleji s Nimbus"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +201,12 @@
|
||||
"download_source_errored": "Fejlede",
|
||||
"sync_download_sources": "Synkronisér kilder",
|
||||
"removed_download_source": "Download kilde fjernet",
|
||||
"cancel_button_confirmation_delete_all_sources": "Ingen",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ja, slet alt",
|
||||
"description_confirmation_delete_all_sources": "Du vil slette alle downloadkilder",
|
||||
"title_confirmation_delete_all_sources": "Slet alle downloadkilder",
|
||||
"removed_download_sources": "Download kilder fjernet",
|
||||
"button_delete_all_sources": "Fjern alle downloadkilder",
|
||||
"added_download_source": "Tilføjede download kilde",
|
||||
"download_sources_synced": "Alle download kilder er synkroniserede",
|
||||
"insert_valid_json_url": "Indsæt en gyldig JSON url",
|
||||
|
||||
@@ -185,6 +185,12 @@
|
||||
"download_source_errored": "Fehlgeschlagen",
|
||||
"sync_download_sources": "Quellen synchronisieren",
|
||||
"removed_download_source": "Download-Quelle entfernt",
|
||||
"cancel_button_confirmation_delete_all_sources": "Nein",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ja, alles löschen",
|
||||
"description_confirmation_delete_all_sources": "Du löschen alle Downloadquellen",
|
||||
"title_confirmation_delete_all_sources": "Löschen du alle Downloadquellen",
|
||||
"removed_download_sources": "Download-Quellen entfernt",
|
||||
"button_delete_all_sources": "Entfernen Sie alle Downloadquellen",
|
||||
"added_download_source": "Download-Quelle hinzugefügt",
|
||||
"download_sources_synced": "Alle Download-Quellen sind synchronisiert",
|
||||
"insert_valid_json_url": "Füge eine gültige JSON URL ein",
|
||||
|
||||
@@ -44,7 +44,10 @@
|
||||
"downloading_metadata": "Downloading {{title}} metadata…",
|
||||
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Completion {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…",
|
||||
"checking_files": "Checking {{title}} files… ({{percentage}} complete)"
|
||||
"checking_files": "Checking {{title}} files… ({{percentage}} complete)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "Installation complete",
|
||||
"installation_complete_message": "Common redistributables installed successfully"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "Filter…",
|
||||
@@ -193,7 +196,8 @@
|
||||
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
||||
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available.",
|
||||
"game_removed_from_favorites": "Game removed from favorites",
|
||||
"game_added_to_favorites": "Game added to favorites"
|
||||
"game_added_to_favorites": "Game added to favorites",
|
||||
"automatically_extract_downloaded_files": "Automatically extract downloaded files"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activate Hydra",
|
||||
@@ -230,7 +234,9 @@
|
||||
"seeding": "Seeding",
|
||||
"stop_seeding": "Stop seeding",
|
||||
"resume_seeding": "Resume seeding",
|
||||
"options": "Manage"
|
||||
"options": "Manage",
|
||||
"extract": "Extract files",
|
||||
"extracting": "Extracting files…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Downloads path",
|
||||
@@ -267,6 +273,12 @@
|
||||
"download_source_errored": "Errored",
|
||||
"sync_download_sources": "Sync sources",
|
||||
"removed_download_source": "Download source removed",
|
||||
"removed_download_sources": "Download sources removed",
|
||||
"cancel_button_confirmation_delete_all_sources": "No",
|
||||
"confirm_button_confirmation_delete_all_sources": "Yes, delete everything",
|
||||
"title_confirmation_delete_all_sources": "Delete all download sources",
|
||||
"description_confirmation_delete_all_sources": "You will delete all download sources",
|
||||
"button_delete_all_sources": "Remove all",
|
||||
"added_download_source": "Added download source",
|
||||
"download_sources_synced": "All download sources are synced",
|
||||
"insert_valid_json_url": "Insert a valid JSON url",
|
||||
@@ -329,12 +341,21 @@
|
||||
"enable_torbox": "Enable Torbox",
|
||||
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
||||
"torbox_account_linked": "TorBox account linked",
|
||||
"create_real_debrid_account": "Click here if you don't have a Real-Debrid account yet",
|
||||
"create_torbox_account": "Click here if you don't have a TorBox account yet",
|
||||
"real_debrid_account_linked": "Real-Debrid account linked",
|
||||
"name_min_length": "Theme name must be at least 3 characters long",
|
||||
"import_theme": "Import theme",
|
||||
"import_theme_description": "You will import {{theme}} from the theme store",
|
||||
"error_importing_theme": "Error importing theme",
|
||||
"theme_imported": "Theme imported successfully"
|
||||
"theme_imported": "Theme imported successfully",
|
||||
"enable_friend_request_notifications": "When a friend request is received",
|
||||
"enable_auto_install": "Download updates automatically",
|
||||
"common_redist": "Common redistributables",
|
||||
"common_redist_description": "Common redistributables are required to run some games. Installing them is recommended to avoid issues.",
|
||||
"install_common_redist": "Install",
|
||||
"installing_common_redist": "Installing…",
|
||||
"show_download_speed_in_megabytes": "Show download speed in megabytes per second"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
@@ -345,13 +366,19 @@
|
||||
"new_update_available": "Version {{version}} available",
|
||||
"restart_to_install_update": "Restart Hydra to install the update",
|
||||
"notification_achievement_unlocked_title": "Achievement unlocked for {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} and other {{count}} were unlocked"
|
||||
"notification_achievement_unlocked_body": "{{achievement}} and other {{count}} were unlocked",
|
||||
"new_friend_request_description": "You have received a new friend request",
|
||||
"new_friend_request_title": "New friend request",
|
||||
"extraction_complete": "Extraction complete",
|
||||
"game_extracted": "{{title}} extracted successfully"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Open Hydra",
|
||||
"quit": "Quit"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Available",
|
||||
"available_other": "Available",
|
||||
"no_downloads": "No downloads available"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
@@ -472,6 +499,7 @@
|
||||
"animated_profile_banner": "Animated profile banner",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "You've just discovered a Hydra Cloud feature!",
|
||||
"learn_more": "Learn More"
|
||||
"learn_more": "Learn More",
|
||||
"debrid_description": "Download up to 4x faster with Nimbus"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +267,12 @@
|
||||
"download_source_errored": "Error",
|
||||
"sync_download_sources": "Sincronizar fuentes",
|
||||
"removed_download_source": "Fuente de descarga eliminada",
|
||||
"cancel_button_confirmation_delete_all_sources": "No",
|
||||
"confirm_button_confirmation_delete_all_sources": "Sí, eliminar todo",
|
||||
"description_confirmation_delete_all_sources": "Eliminarás todas las fuentes de descarga",
|
||||
"title_confirmation_delete_all_sources": "Eliminar todas las fuentes de descarga",
|
||||
"removed_download_sources": "Fuentes de descarga eliminadas",
|
||||
"button_delete_all_sources": "Eliminar todas las fuentes de descarga",
|
||||
"added_download_source": "Fuente de descarga añadida",
|
||||
"download_sources_synced": "Todas las fuentes de descargas están actualizadas.",
|
||||
"insert_valid_json_url": "Introduce una URL JSON válida",
|
||||
@@ -298,7 +304,6 @@
|
||||
"no_email_account": "No has configurado un correo aún",
|
||||
"no_subscription": "Disfruta Hydra de la mejor manera",
|
||||
"no_users_blocked": "No tienes usuarios bloqueados",
|
||||
"notifications": "Notificaciones",
|
||||
"renew_subscription": "Renovar Hydra Cloud",
|
||||
"subscription_active_until": "Tu Hydra Cloud está activa hasta {{date}}",
|
||||
"subscription_expired_at": "Tú suscripción expiró el {{date}}",
|
||||
@@ -335,7 +340,9 @@
|
||||
"torbox_account_linked": "Cuenta de TorBox vinculada",
|
||||
"torbox_description": "TorBox es tu servicio premium de seedbox que rivaliza incluso a los mejores servidores del mercado.",
|
||||
"unset_theme": "Desactivar tema",
|
||||
"web_store": "Tienda Web"
|
||||
"web_store": "Tienda Web",
|
||||
"enable_friend_request_notifications": "Cuando se recibe una solicitud de amistad",
|
||||
"enable_auto_install": "Descargar actualizaciones automáticamente"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Descarga completada",
|
||||
@@ -346,13 +353,17 @@
|
||||
"new_update_available": "Version {{version}} disponible",
|
||||
"restart_to_install_update": "Reinicia Hydra para instalar la actualización",
|
||||
"notification_achievement_unlocked_title": "Logro desbloqueado de {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados"
|
||||
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados",
|
||||
"new_friend_request_title": "Nueva solicitud de amistad",
|
||||
"new_friend_request_description": "Has recibido una nueva solicitud de amistad"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Abrir Hydra",
|
||||
"quit": "Salir"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Disponible",
|
||||
"available_other": "Disponibles",
|
||||
"no_downloads": "No hay descargas disponibles"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
|
||||
@@ -236,6 +236,12 @@
|
||||
"download_source_errored": "Vigane",
|
||||
"sync_download_sources": "Sünkroniseeri allikad",
|
||||
"removed_download_source": "Allalaadimise allikas eemaldatud",
|
||||
"cancel_button_confirmation_delete_all_sources": "Ei",
|
||||
"confirm_button_confirmation_delete_all_sources": "Jah, kustuta kõik",
|
||||
"description_confirmation_delete_all_sources": "Kustutate kõik allalaadimisallikad",
|
||||
"title_confirmation_delete_all_sources": "Kustutage kõik allalaadimisallikad",
|
||||
"removed_download_sources": "Allalaadimise allikas eemaldati",
|
||||
"button_delete_all_sources": "Eemaldage kõik allalaadimisallikad",
|
||||
"added_download_source": "Allalaadimise allikas lisatud",
|
||||
"download_sources_synced": "Kõik allalaadimise allikad on sünkroniseeritud",
|
||||
"insert_valid_json_url": "Sisesta kehtiv JSON url",
|
||||
|
||||
@@ -111,6 +111,12 @@
|
||||
"launch_with_system": "زمانی که سیستم روشن میشود، هایدرا را باز کن",
|
||||
"general": "کلی",
|
||||
"behavior": "رفتار",
|
||||
"cancel_button_confirmation_delete_all_sources": "خیر",
|
||||
"confirm_button_confirmation_delete_all_sources": "ہاں، سب کچھ حذف کر دیں۔",
|
||||
"description_confirmation_delete_all_sources": "آپ ڈاؤن لوڈ کے تمام ذرائع کو حذف کر دیں گے۔",
|
||||
"title_confirmation_delete_all_sources": "تمام منابع دانلود را حذف کنید",
|
||||
"removed_download_sources": "منابع دانلود حذف شد",
|
||||
"button_delete_all_sources": "تمام منابع دانلود را حذف کنید",
|
||||
"enable_real_debrid": "فعالسازی Real-Debrid",
|
||||
"debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.",
|
||||
"save_changes": "ذخیره تغییرات"
|
||||
|
||||
@@ -99,7 +99,13 @@
|
||||
"notifications": "Notifications",
|
||||
"enable_download_notifications": "Quand un téléchargement est terminé",
|
||||
"enable_repack_list_notifications": "Quand un nouveau repack est ajouté",
|
||||
"language": "Langue"
|
||||
"language": "Langue",
|
||||
"cancel_button_confirmation_delete_all_sources": "Non",
|
||||
"confirm_button_confirmation_delete_all_sources": "Oui, tout supprimer",
|
||||
"description_confirmation_delete_all_sources": "Vous supprimerez toutes les sources de téléchargement",
|
||||
"title_confirmation_delete_all_sources": "Supprimer toutes les sources de téléchargement",
|
||||
"removed_download_sources": "Sources de téléchargement supprimées",
|
||||
"button_delete_all_sources": "Supprimer toutes les sources de téléchargement"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Téléchargement terminé",
|
||||
|
||||
@@ -106,6 +106,12 @@
|
||||
"change": "Frissítés",
|
||||
"notifications": "Értesítések",
|
||||
"enable_download_notifications": "Amikor egy letöltés befejeződik",
|
||||
"cancel_button_confirmation_delete_all_sources": "Nem",
|
||||
"confirm_button_confirmation_delete_all_sources": "Igen, törölj mindent",
|
||||
"description_confirmation_delete_all_sources": "Törölni fog minden letöltési forrást",
|
||||
"title_confirmation_delete_all_sources": "Törölje az összes letöltési forrást",
|
||||
"removed_download_sources": "Betűtípusok eltávolítva",
|
||||
"button_delete_all_sources": "Távolítsa el az összes letöltési forrást",
|
||||
"enable_repack_list_notifications": "Amikor egy új repack hozzáadásra kerül"
|
||||
},
|
||||
"notifications": {
|
||||
|
||||
@@ -175,6 +175,12 @@
|
||||
"download_sources_description": "Hydra akan mencari link unduhan dari sini. URL harus menuju file .json dengan link unduhan.",
|
||||
"validate_download_source": "Validasi",
|
||||
"remove_download_source": "Hapus",
|
||||
"cancel_button_confirmation_delete_all_sources": "TIDAK",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ya, hapus semuanya",
|
||||
"description_confirmation_delete_all_sources": "Anda akan menghapus semua sumber unduhan",
|
||||
"title_confirmation_delete_all_sources": "Hapus semua sumber unduhan",
|
||||
"removed_download_sources": "Font dihapus",
|
||||
"button_delete_all_sources": "Hapus semua sumber unduhan",
|
||||
"add_download_source": "Tambahkan sumber",
|
||||
"download_count_zero": "Tidak ada unduhan dalam daftar",
|
||||
"download_count_one": "{{countFormatted}} unduhan dalam daftar",
|
||||
|
||||
@@ -120,6 +120,12 @@
|
||||
"general": "Generale",
|
||||
"behavior": "Comportamento",
|
||||
"enable_real_debrid": "Abilita Real Debrid",
|
||||
"cancel_button_confirmation_delete_all_sources": "NO",
|
||||
"confirm_button_confirmation_delete_all_sources": "Sì, cancella tutto",
|
||||
"description_confirmation_delete_all_sources": "Eliminerai tutte le fonti di download",
|
||||
"title_confirmation_delete_all_sources": "Elimina tutte le fonti di download",
|
||||
"removed_download_sources": "Fonti di download rimosse",
|
||||
"button_delete_all_sources": "Rimuovi tutte le fonti di download",
|
||||
"debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here</0>",
|
||||
"save_changes": "Salva modifiche"
|
||||
},
|
||||
|
||||
@@ -173,6 +173,12 @@
|
||||
"download_sources_description": "Hydra осы көздерден жүктеу сілтемелерін алады. URL-да жүктеу сілтемелері бар .json файлына тікелей сілтеме болуы керек.",
|
||||
"validate_download_source": "Тексеру",
|
||||
"remove_download_source": "Жою",
|
||||
"cancel_button_confirmation_delete_all_sources": "Жоқ",
|
||||
"confirm_button_confirmation_delete_all_sources": "Иә, бәрін жойыңыз",
|
||||
"description_confirmation_delete_all_sources": "Барлық жүктеу көздерін жоясыз",
|
||||
"title_confirmation_delete_all_sources": "Барлық жүктеу көздерін жойыңыз",
|
||||
"removed_download_sources": "Қаріптер жойылды",
|
||||
"button_delete_all_sources": "Барлық жүктеу көздерін жойыңыз",
|
||||
"add_download_source": "Жүктеу көзін қосу",
|
||||
"download_count_zero": "Жүктеулер тізімінде жоқ",
|
||||
"download_count_one": "{{countFormatted}} жүктеу тізімде",
|
||||
|
||||
@@ -111,6 +111,12 @@
|
||||
"launch_with_system": "컴퓨터가 시작되었을 때 Hydra 실행",
|
||||
"general": "일반",
|
||||
"behavior": "행동",
|
||||
"cancel_button_confirmation_delete_all_sources": "아니요",
|
||||
"confirm_button_confirmation_delete_all_sources": "네, 모두 삭제합니다",
|
||||
"description_confirmation_delete_all_sources": "모든 다운로드 소스를 삭제합니다.",
|
||||
"title_confirmation_delete_all_sources": "모든 다운로드 소스 삭제",
|
||||
"removed_download_sources": "제거된 글꼴",
|
||||
"button_delete_all_sources": "모든 다운로드 소스 제거",
|
||||
"enable_real_debrid": "Real-Debrid 활성화",
|
||||
"debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.",
|
||||
"save_changes": "변경 사항 저장"
|
||||
|
||||
@@ -201,6 +201,12 @@
|
||||
"download_source_errored": "Mislyktes",
|
||||
"sync_download_sources": "Synkroniser kilder",
|
||||
"removed_download_source": "Nedlastingskilde fjernet",
|
||||
"cancel_button_confirmation_delete_all_sources": "Ingen",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ja, slett alt",
|
||||
"description_confirmation_delete_all_sources": "Du vil slette alle nedlastingskilder",
|
||||
"title_confirmation_delete_all_sources": "Slett alle nedlastingskilder",
|
||||
"removed_download_sources": "Fonter fjernet",
|
||||
"button_delete_all_sources": "Fjern alle nedlastingskilder",
|
||||
"added_download_source": "La til Nedlastingskilde",
|
||||
"download_sources_synced": "Alle nedlastingskilder er synkroniserte",
|
||||
"insert_valid_json_url": "Innsett en gyldig JSON url",
|
||||
|
||||
@@ -112,6 +112,12 @@
|
||||
"launch_with_system": "Start Hydra bij het opstarten van het systeem",
|
||||
"general": "Algemeen",
|
||||
"behavior": "Gedrag",
|
||||
"cancel_button_confirmation_delete_all_sources": "Nee",
|
||||
"confirm_button_confirmation_delete_all_sources": "Ja, verwijder alles",
|
||||
"description_confirmation_delete_all_sources": "Je verwijdert alle downloadbronnen",
|
||||
"title_confirmation_delete_all_sources": "Verwijder alle downloadbronnen",
|
||||
"removed_download_sources": "Downloadbronnen verwijderd",
|
||||
"button_delete_all_sources": "Verwijder alle downloadbronnen",
|
||||
"enable_real_debrid": "Enable Real-Debrid",
|
||||
"debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
|
||||
"save_changes": "Wijzigingen opslaan"
|
||||
|
||||
@@ -120,6 +120,11 @@
|
||||
"general": "Ogólne",
|
||||
"behavior": "Zachowania",
|
||||
"language": "Język",
|
||||
"cancel_button_confirmation_delete_all_sources": "NIE",
|
||||
"confirm_button_confirmation_delete_all_sources": "Tak, usuń wszystko",
|
||||
"description_confirmation_delete_all_sources": "Usuniesz wszystkie źródła pobierania",
|
||||
"title_confirmation_delete_all_sources": "Usuń wszystkie źródła pobierania",
|
||||
"button_delete_all_sources": "Usuń wszystkie źródła pobierania",
|
||||
"enable_real_debrid": "Włącz Real-Debrid",
|
||||
"debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>",
|
||||
"save_changes": "Zapisz zmiany"
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar jogos",
|
||||
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Downloads",
|
||||
"search_results": "Resultados da busca",
|
||||
@@ -45,7 +44,10 @@
|
||||
"downloading_metadata": "Baixando metadados de {{title}}…",
|
||||
"downloading": "Baixando {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Baixando {{title}}… ({{percentage}} concluído) - Calculando tempo restante…",
|
||||
"checking_files": "Verificando arquivos de {{title}}…"
|
||||
"checking_files": "Verificando arquivos de {{title}}…",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "Instalação concluída",
|
||||
"installation_complete_message": "Componentes recomendados instalados com sucesso"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Ver opções de download",
|
||||
@@ -183,7 +185,8 @@
|
||||
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
|
||||
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível.",
|
||||
"game_removed_from_favorites": "Jogo removido dos favoritos",
|
||||
"game_added_to_favorites": "Jogo adicionado aos favoritos"
|
||||
"game_added_to_favorites": "Jogo adicionado aos favoritos",
|
||||
"automatically_extract_downloaded_files": "Extrair automaticamente os arquivos baixados"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
@@ -220,7 +223,9 @@
|
||||
"seeding": "Semeando",
|
||||
"stop_seeding": "Parar de semear",
|
||||
"resume_seeding": "Semear",
|
||||
"options": "Gerenciar"
|
||||
"options": "Gerenciar",
|
||||
"extract": "Extrair arquivos",
|
||||
"extracting": "Extraindo arquivos…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Diretório dos downloads",
|
||||
@@ -257,6 +262,12 @@
|
||||
"download_source_errored": "Falhou",
|
||||
"sync_download_sources": "Sincronizar",
|
||||
"removed_download_source": "Fonte removida",
|
||||
"removed_download_sources": "Fontes removidas",
|
||||
"cancel_button_confirmation_delete_all_sources": "Não",
|
||||
"confirm_button_confirmation_delete_all_sources": "Sim, excluir tudo",
|
||||
"title_confirmation_delete_all_sources": "Remover todas as fontes de download",
|
||||
"description_confirmation_delete_all_sources": "Você irá remover todas as fontes de download. Deseja prosseguir?",
|
||||
"button_delete_all_sources": "Remover todas",
|
||||
"added_download_source": "Fonte adicionada",
|
||||
"download_sources_synced": "As fontes foram sincronizadas",
|
||||
"insert_valid_json_url": "Insira a url de um JSON válido",
|
||||
@@ -317,12 +328,21 @@
|
||||
"enable_torbox": "Habilitar Torbox",
|
||||
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
|
||||
"torbox_account_linked": "Conta do TorBox vinculada",
|
||||
"create_real_debrid_account": "Clique aqui se você ainda não tem uma conta do Real-Debrid",
|
||||
"create_torbox_account": "Clique aqui se você ainda não tem uma conta do TorBox",
|
||||
"real_debrid_account_linked": "Conta Real-Debrid associada",
|
||||
"name_min_length": "O nome do tema deve ter pelo menos 3 caracteres",
|
||||
"import_theme": "Importar tema",
|
||||
"import_theme_description": "Você irá importar {{theme}} da loja de temas",
|
||||
"error_importing_theme": "Erro ao importar tema",
|
||||
"theme_imported": "Tema importado com sucesso"
|
||||
"theme_imported": "Tema importado com sucesso",
|
||||
"enable_friend_request_notifications": "Quando um pedido de amizade é recebido",
|
||||
"enable_auto_install": "Baixar atualizações automaticamente",
|
||||
"common_redist": "Componentes recomendados",
|
||||
"common_redist_description": "Componentes recomendados são necessários para executar alguns jogos. A instalação deles é recomendada para evitar problemas.",
|
||||
"install_common_redist": "Instalar",
|
||||
"installing_common_redist": "Instalando…",
|
||||
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download concluído",
|
||||
@@ -331,13 +351,19 @@
|
||||
"repack_count_one": "{{count}} novo repack",
|
||||
"repack_count_other": "{{count}} novos repacks",
|
||||
"new_update_available": "Versão {{version}} disponível",
|
||||
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão"
|
||||
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão",
|
||||
"new_friend_request_title": "Novo pedido de amizade",
|
||||
"new_friend_request_description": "Você recebeu um novo pedido de amizade",
|
||||
"extraction_complete": "Extração concluída",
|
||||
"game_extracted": "{{title}} extraído com sucesso"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Abrir Hydra",
|
||||
"quit": "Fechar"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Disponível",
|
||||
"available_other": "Disponíveis",
|
||||
"no_downloads": "Sem downloads disponíveis"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
@@ -468,6 +494,7 @@
|
||||
"animated_profile_banner": "Banner animado no perfil",
|
||||
"cloud_saving": "Saves de jogos em nuvem",
|
||||
"hydra_cloud_feature_found": "Você descobriu uma funcionalidade Hydra Cloud!",
|
||||
"learn_more": "Saiba mais"
|
||||
"learn_more": "Saiba mais",
|
||||
"debrid_description": "Baixe até 4x mais rápido com Nimbus"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
},
|
||||
"header": {
|
||||
"search": "Procurar jogos",
|
||||
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Transferências",
|
||||
"search_results": "Resultados da pesquisa",
|
||||
@@ -103,7 +102,7 @@
|
||||
"open_download_location": "Ver ficheiros transferidos",
|
||||
"create_shortcut": "Criar atalho no ambiente de trabalho",
|
||||
"remove_files": "Remover ficheiros",
|
||||
"options": "Gerir",
|
||||
"options": "Opções",
|
||||
"remove_from_library_description": "Isto vai remover {{game}} da tua biblioteca",
|
||||
"remove_from_library_title": "Tens a certeza?",
|
||||
"executable_section_title": "Executável",
|
||||
@@ -159,7 +158,27 @@
|
||||
"no_download_option_info": "Sem informações disponíveis",
|
||||
"backup_deletion_failed": "Falha ao apagar o backup",
|
||||
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
|
||||
"achievements_not_sync": "As tuas conquistas não estão sincronizadas"
|
||||
"achievements_not_sync": "As tuas conquistas não estão sincronizadas",
|
||||
"backup_from": "Backup de {{date}}",
|
||||
"automatic_backup_from": "Backup automático de {{date}}",
|
||||
"enable_automatic_cloud_sync": "Ativar sincronização automática na nuvem",
|
||||
"custom_backup_location_set": "Localização customizada selecionada",
|
||||
"select_folder": "Selecione a pasta",
|
||||
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
|
||||
"clear": "Limpar",
|
||||
"no_directory_selected": "Nenhum diretório selecionado",
|
||||
"reset_achievements": "Repor conquistas",
|
||||
"reset_achievements_description": "Isto irá apagar todas as conquistas de {{game}}",
|
||||
"reset_achievements_title": "Tem certeza?",
|
||||
"reset_achievements_success": "Conquistas repostas com sucesso",
|
||||
"reset_achievements_error": "Falha ao repor conquistas",
|
||||
"no_write_permission": "Não é possível descarregar neste diretório. Clique aqui para saber mais.",
|
||||
"download_error_gofile_quota_exceeded": "Você excedeu sua cota mensal do Gofile. Por favor, aguarde o reset da cota.",
|
||||
"download_error_real_debrid_account_not_authorized": "A sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique a sua assinatura e tente novamente.",
|
||||
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
|
||||
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível.",
|
||||
"game_removed_from_favorites": "Jogo removido dos favoritos",
|
||||
"game_added_to_favorites": "Jogo adicionado aos favoritos"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
@@ -192,7 +211,11 @@
|
||||
"queued": "Na fila",
|
||||
"no_downloads_title": "Nada por aqui…",
|
||||
"no_downloads_description": "Ainda não descarregaste nada pelo Hydra, mas nunca é tarde para começar.",
|
||||
"checking_files": "A verificar ficheiros…"
|
||||
"checking_files": "A verificar ficheiros…",
|
||||
"seeding": "A semear",
|
||||
"stop_seeding": "Parar de semear",
|
||||
"resume_seeding": "Semear",
|
||||
"options": "Opções"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Local das transferências",
|
||||
@@ -232,6 +255,12 @@
|
||||
"download_source_errored": "Falhou",
|
||||
"sync_download_sources": "Sincronizar",
|
||||
"removed_download_source": "Fonte removida",
|
||||
"cancel_button_confirmation_delete_all_sources": "Não",
|
||||
"confirm_button_confirmation_delete_all_sources": "Sim, apague tudo",
|
||||
"description_confirmation_delete_all_sources": "Irá remover todas as fontes de download",
|
||||
"title_confirmation_delete_all_sources": "Remover todas as fontes de download",
|
||||
"removed_download_sources": "Fontes de download removidas",
|
||||
"button_delete_all_sources": "Remover todas",
|
||||
"added_download_source": "Fonte adicionada",
|
||||
"download_sources_synced": "As fontes foram sincronizadas",
|
||||
"insert_valid_json_url": "Insere o URL de um JSON válido",
|
||||
@@ -250,7 +279,57 @@
|
||||
"must_be_valid_url": "A fonte deve ser um URL válido",
|
||||
"blocked_users": "Utilizadores bloqueados",
|
||||
"user_unblocked": "Utilizador desbloqueado",
|
||||
"enable_achievement_notifications": "Quando uma conquista é desbloqueada"
|
||||
"enable_achievement_notifications": "Quando uma conquista é desbloqueada",
|
||||
"enable_friend_request_notifications": "Quando um pedido de amizade é recebido",
|
||||
"launch_minimized": "Iniciar Hydra minimizado",
|
||||
"disable_nsfw_alert": "Desativar alertas NSFW",
|
||||
"seed_after_download_complete": "Semear após concluir o download",
|
||||
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de as desbloquear",
|
||||
"account": "Conta",
|
||||
"no_users_blocked": "Não tem utilizadores bloqueados",
|
||||
"subscription_active_until": "O teu Hydra Cloud está ativo até {{date}}",
|
||||
"manage_subscription": "Gerir subscrições",
|
||||
"update_email": "Atualizar email",
|
||||
"update_password": "Atualizar password",
|
||||
"current_email": "Email atual:",
|
||||
"no_email_account": "Ainda não adicionou nenhum email à sua conta",
|
||||
"account_data_updated_successfully": "Dados da conta atualizados com sucesso",
|
||||
"renew_subscription": "Renovar Hydra Cloud",
|
||||
"subscription_expired_at": "A sua subscrição expirou a {{date}}",
|
||||
"no_subscription": "Aproveite o Hydra da melhor forma possível",
|
||||
"become_subscriber": "Subscreva o Hydra Cloud",
|
||||
"subscription_renew_cancelled": "A renovação automática está desativada",
|
||||
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
||||
"bill_sent_until": "A próxima cobrança será enviada até esse dia",
|
||||
"no_themes": "Parece que ainda não tem nenhum tema. Não se preocupe, clique aqui para criar a sua primeira obra de arte.",
|
||||
"editor_tab_code": "Código",
|
||||
"editor_tab_info": "Info",
|
||||
"editor_tab_save": "Guardar",
|
||||
"web_store": "Loja de temas",
|
||||
"clear_themes": "Limpar",
|
||||
"create_theme": "Criar",
|
||||
"create_theme_modal_title": "Criar tema customizado",
|
||||
"create_theme_modal_description": "Criar um novo tema para customizar a aparência do Hydra",
|
||||
"theme_name": "Nome",
|
||||
"insert_theme_name": "Insira o nome do tema",
|
||||
"set_theme": "Definir tema",
|
||||
"unset_theme": "Redefinir tema",
|
||||
"delete_theme": "Apagar tema",
|
||||
"edit_theme": "Editar tema",
|
||||
"delete_all_themes": "Apagar todos os temas",
|
||||
"delete_all_themes_description": "Isto irá apagar todos os seus temas",
|
||||
"delete_theme_description": "Isto irá apagar o tema {{theme}}",
|
||||
"cancel": "Cancelar",
|
||||
"appearance": "Aparência",
|
||||
"enable_torbox": "Ativar Torbox",
|
||||
"torbox_description": "TorBox é um serviço de seedbox premium sendo um dos melhores servidores do mercado.",
|
||||
"torbox_account_linked": "Conta do TorBox associada",
|
||||
"real_debrid_account_linked": "Conta Real-Debrid associada",
|
||||
"name_min_length": "O nome do tema deve ter pelo menos 3 caracteres",
|
||||
"import_theme": "Importar tema",
|
||||
"import_theme_description": "Irá importar {{theme}} da loja de temas",
|
||||
"error_importing_theme": "Erro ao importar tema",
|
||||
"theme_imported": "Tema importado com sucesso"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Transferência concluída",
|
||||
@@ -259,13 +338,17 @@
|
||||
"repack_count_one": "{{count}} novo repack",
|
||||
"repack_count_other": "{{count}} novos repacks",
|
||||
"new_update_available": "Versão {{version}} disponível",
|
||||
"restart_to_install_update": "Reinicia o Hydra para instalar a nova versão"
|
||||
"restart_to_install_update": "Reinicia o Hydra para instalar a nova versão",
|
||||
"new_friend_request_title": "Novo pedido de amizade",
|
||||
"new_friend_request_description": "Recebeste um novo pedido de amizade"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Abrir o Hydra",
|
||||
"quit": "Sair"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Disponível",
|
||||
"available_other": "Disponíveis",
|
||||
"no_downloads": "Sem downloads disponíveis"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
@@ -275,7 +358,16 @@
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Página seguinte",
|
||||
"previous_page": "Página anterior"
|
||||
"previous_page": "Página anterior",
|
||||
"search": "Filtrar…",
|
||||
"developers": "Desenvolvedores",
|
||||
"genres": "Géneros",
|
||||
"tags": "Marcadores",
|
||||
"publishers": "Distribuidoras",
|
||||
"download_sources": "Fontes de download",
|
||||
"result_count": "{{resultCount}} resultados",
|
||||
"filter_count": "{{filterCount}} disponíveis",
|
||||
"clear_filters": "Limpar {{filterCount}} selecionados"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Botão de fechar"
|
||||
@@ -352,7 +444,17 @@
|
||||
"profile_reported": "Perfil denunciado",
|
||||
"your_friend_code": "O teu código de amigo:",
|
||||
"upload_banner": "Fazer upload do banner",
|
||||
"uploading_banner": "A fazer upload do banner…"
|
||||
"uploading_banner": "A fazer upload do banner…",
|
||||
"background_image_updated": "Imagem de fundo salva",
|
||||
"stats": "Estatísticas",
|
||||
"achievements": "conquistas",
|
||||
"games": "Jogos",
|
||||
"ranking_updated_weekly": "O ranking é atualizado semanalmente",
|
||||
"playing": "A jogar {{game}}",
|
||||
"achievements_unlocked": "Conquistas desbloqueadas",
|
||||
"earned_points": "Pontos ganhos",
|
||||
"show_achievements_on_profile": "Mostre as suas conquistas no perfil",
|
||||
"show_points_on_profile": "Mostre os seus pontos ganhos no perfil"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Conquista desbloqueada",
|
||||
@@ -360,15 +462,25 @@
|
||||
"user_achievements": "Conquistas de {{displayName}}",
|
||||
"unlocked_at": "Desbloqueada em: {{date}}",
|
||||
"subscription_needed": "Precisas de uma subscrição Hydra Cloud para visualizar este conteúdo",
|
||||
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos"
|
||||
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
|
||||
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "Esta é uma conquista oculta",
|
||||
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
|
||||
"earned_points": "Pontos ganhos:",
|
||||
"available_points": "Pontos disponíveis:",
|
||||
"how_to_earn_achievements_points": "Como desbloquear pontos nas conquistas?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Subscrição Hydra Cloud",
|
||||
"subscribe_now": "Subscreve agora",
|
||||
"cloud_achievements": "Gravação de conquistas na nuvem",
|
||||
"animated_profile_picture": "Fotos de perfil animadas",
|
||||
"premium_support": "Apoio Premium",
|
||||
"premium_support": "Suporte Premium",
|
||||
"show_and_compare_achievements": "Mostra e compara as tuas conquistas com as de outros utilizadores",
|
||||
"animated_profile_banner": "Banner animado no perfil"
|
||||
"animated_profile_banner": "Banner animado no perfil",
|
||||
"cloud_saving": "Progresso dos jogos na nuvem",
|
||||
"hydra_cloud_feature_found": "Descubriste uma funcionalidade Hydra Cloud!",
|
||||
"learn_more": "Saber mais"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
"behavior": "Comportament",
|
||||
"language": "Limbă",
|
||||
"api_token": "Token API",
|
||||
"cancel_button_confirmation_delete_all_sources": "Nu",
|
||||
"confirm_button_confirmation_delete_all_sources": "Da, șterge totul",
|
||||
"description_confirmation_delete_all_sources": "Veți șterge toate sursele de descărcare",
|
||||
"title_confirmation_delete_all_sources": "Ștergeți toate sursele de descărcare",
|
||||
"removed_download_sources": "Sursele de descărcare au fost eliminate",
|
||||
"button_delete_all_sources": "Eliminați toate sursele de descărcare",
|
||||
"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.",
|
||||
"debrid_invalid_token": "Token API invalid",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"hot": "Сейчас популярно",
|
||||
"start_typing": "Начинаю вводить текст...",
|
||||
"weekly": "📅 Лучшие игры недели",
|
||||
"achievements": "🏆 Игры, в которых нужно победить"
|
||||
"achievements": "🏆 Игры с достижениями"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Каталог",
|
||||
@@ -36,7 +36,7 @@
|
||||
"downloads": "Загрузки",
|
||||
"search_results": "Результаты поиска",
|
||||
"settings": "Настройки",
|
||||
"version_available_install": "Доступна версия {{version}}. Нажмите здесь для перезапуска и установки.",
|
||||
"version_available_install": "Доступна версия {{version}}. Нажмите здесь для установки.",
|
||||
"version_available_download": "Доступна версия {{version}}. Нажмите здесь для загрузки."
|
||||
},
|
||||
"bottom_panel": {
|
||||
@@ -44,13 +44,16 @@
|
||||
"downloading_metadata": "Загрузка метаданных {{title}}…",
|
||||
"downloading": "Загрузка {{title}}… ({{percentage}} завершено) - Окончание {{eta}} - {{speed}}",
|
||||
"calculating_eta": "Загрузка {{title}}… ({{percentage}} завершено) - Подсчёт оставшегося времени…",
|
||||
"checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)"
|
||||
"checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "Установка завершена",
|
||||
"installation_complete_message": "Библиотеки успешно установлены"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "Фильтр…",
|
||||
"developers": "Разработчики",
|
||||
"genres": "Жанры",
|
||||
"tags": "Маркеры",
|
||||
"tags": "Теги",
|
||||
"publishers": "Издательства",
|
||||
"download_sources": "Источники загрузки",
|
||||
"result_count": "{{resultCount}} результатов",
|
||||
@@ -183,15 +186,18 @@
|
||||
"custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии",
|
||||
"no_directory_selected": "Не выбран каталог",
|
||||
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
|
||||
"reset_achievements": "Сброс достижений",
|
||||
"reset_achievements_description": "Это сбросит все достижения для {{game}}",
|
||||
"reset_achievements_title": "Вы уверены?",
|
||||
"reset_achievements_success": "Достижения успешно сброшены",
|
||||
"reset_achievements_error": "Не удалось сбросить достижения",
|
||||
"download_error_gofile_quota_exceeded": "Вы превысили месячную квоту Gofile. Пожалуйста, подождите, пока квота не будет восстановлена.",
|
||||
"download_error_real_debrid_account_not_authorized": "Ваш аккаунт Real-Debrid не авторизован для осуществления новых загрузок. Пожалуйста, проверьте настройки учетной записи и повторите попытку.",
|
||||
"download_error_not_cached_in_real_debrid": "Эта загрузка недоступна на Real-Debrid, а опрос статуса загрузки с Real-Debrid пока недоступен.",
|
||||
"download_error_not_cached_in_torbox": "Эта загрузка недоступна на Torbox, и опросить статус загрузки с Torbox пока невозможно.",
|
||||
"download_error_not_cached_in_real_debrid": "Эта загрузка недоступна на Real-Debrid, и получение статуса загрузки с Real-Debrid пока недоступно.",
|
||||
"download_error_not_cached_in_torbox": "Эта загрузка недоступна на Torbox, и получить статус загрузки с Torbox пока невозможно.",
|
||||
"game_added_to_favorites": "Игра добавлена в избранное",
|
||||
"game_removed_from_favorites": "Игра удалена из избранного"
|
||||
"game_removed_from_favorites": "Игра удалена из избранного",
|
||||
"automatically_extract_downloaded_files": "Автоматическая распаковка загруженных файлов"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Активировать Hydra",
|
||||
@@ -228,7 +234,9 @@
|
||||
"seeding": "Раздача",
|
||||
"stop_seeding": "Остановить раздачу",
|
||||
"resume_seeding": "Продолжить раздачу",
|
||||
"options": "Управлять"
|
||||
"options": "Управлять",
|
||||
"extract": "Распаковать файлы",
|
||||
"extracting": "Распаковка файлов…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Путь загрузок",
|
||||
@@ -267,9 +275,15 @@
|
||||
"download_source_up_to_date": "Обновлён",
|
||||
"download_source_errored": "Ошибка",
|
||||
"sync_download_sources": "Обновить источники",
|
||||
"removed_download_source": "Источник загрузок удален",
|
||||
"added_download_source": "Источник загрузок добавлен",
|
||||
"download_sources_synced": "Все источники загрузок синхронизированы",
|
||||
"removed_download_source": "Источник удален",
|
||||
"cancel_button_confirmation_delete_all_sources": "Нет",
|
||||
"confirm_button_confirmation_delete_all_sources": "Да, удалить все",
|
||||
"description_confirmation_delete_all_sources": "Вы удалите все источники",
|
||||
"title_confirmation_delete_all_sources": "Удалить все источники",
|
||||
"removed_download_sources": "Источники удалены",
|
||||
"button_delete_all_sources": "Удалить все источники",
|
||||
"added_download_source": "Источник добавлен",
|
||||
"download_sources_synced": "Все источники обновлены",
|
||||
"insert_valid_json_url": "Вставьте действительный URL JSON-файла",
|
||||
"found_download_option_zero": "Не найдено вариантов загрузки",
|
||||
"found_download_option_one": "Найден {{countFormatted}} вариант загрузки",
|
||||
@@ -277,7 +291,7 @@
|
||||
"import": "Импортировать",
|
||||
"blocked_users": "Заблокированные пользователи",
|
||||
"friends_only": "Только для друзей",
|
||||
"must_be_valid_url": "Источник должен быть действительным URL-адресом.",
|
||||
"must_be_valid_url": "У источника должен быть правильный URL",
|
||||
"privacy": "Конфиденциальность",
|
||||
"private": "Частный",
|
||||
"profile_visibility": "Видимость профиля",
|
||||
@@ -328,11 +342,20 @@
|
||||
"torbox_description": "TorBox - это ваш премиум-сервис, конкурирующий даже с лучшими серверами на рынке.",
|
||||
"torbox_account_linked": "Аккаунт TorBox привязан",
|
||||
"real_debrid_account_linked": "Аккаунт Real-Debrid привязан",
|
||||
"create_real_debrid_account": "Нажмите здесь, если у вас еще нет аккаунта Real-Debrid",
|
||||
"create_torbox_account": "Нажмите здесь, если у вас еще нет учетной записи TorBox",
|
||||
"name_min_length": "Название темы должно содержать не менее 3 символов",
|
||||
"import_theme": "Импортировать тему",
|
||||
"import_theme_description": "Вы импортируете {{theme}} из магазина тем",
|
||||
"error_importing_theme": "Ошибка при импорте темы",
|
||||
"theme_imported": "Тема успешно импортирована"
|
||||
"theme_imported": "Тема успешно импортирована",
|
||||
"enable_friend_request_notifications": "При получении запроса на добавление в друзья",
|
||||
"enable_auto_install": "Загружать обновления автоматически",
|
||||
"common_redist": "Библиотеки",
|
||||
"common_redist_description": "Для запуска некоторых игр требуются библиотеки. Во избежание проблем рекомендуется установить их.",
|
||||
"install_common_redist": "Установить",
|
||||
"installing_common_redist": "Установка…",
|
||||
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Загрузка завершена",
|
||||
@@ -343,13 +366,19 @@
|
||||
"new_update_available": "Доступна новая версия {{version}}",
|
||||
"restart_to_install_update": "Перезапустите Hydra для установки обновления",
|
||||
"notification_achievement_unlocked_title": "Достижение разблокировано для {{game}}",
|
||||
"notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}"
|
||||
"notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}",
|
||||
"new_friend_request_title": "Новый запрос на добавление в друзья",
|
||||
"new_friend_request_description": "Вы получили новый запрос на добавление в друзья",
|
||||
"extraction_complete": "Распаковка завершена",
|
||||
"game_extracted": "{{title}} успешно распакован"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Открыть Hydra",
|
||||
"quit": "Выйти"
|
||||
},
|
||||
"game_card": {
|
||||
"available_one": "Доступный",
|
||||
"available_other": "Доступный",
|
||||
"no_downloads": "Нет доступных источников"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
@@ -434,11 +463,13 @@
|
||||
"uploading_banner": "Загрузка баннера...",
|
||||
"background_image_updated": "Фоновое изображение обновлено",
|
||||
"stats": "Статистика",
|
||||
"achievements": "Достижения",
|
||||
"games": "Игры",
|
||||
"top_percentile": "Топ {{percentile}}%",
|
||||
"ranking_updated_weekly": "Рейтинг обновляется еженедельно",
|
||||
"playing": "Играет в {{game}}",
|
||||
"achievements_unlocked": "Достижения разблокированы",
|
||||
"earned_points": "Заработано очков:",
|
||||
"show_achievements_on_profile": "Покажите свои достижения в профиле",
|
||||
"show_points_on_profile": "Показывать заработанные очки в своем профиле"
|
||||
},
|
||||
@@ -468,6 +499,7 @@
|
||||
"animated_profile_banner": "Анимированный баннер профиля",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Вы только что открыли для себя функцию Hydra Cloud!",
|
||||
"learn_more": "Подробнее"
|
||||
"learn_more": "Подробнее",
|
||||
"debrid_description": "Скачивайте в 4 раза быстрее с Nimbus"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "İndirilenler",
|
||||
"settings": "Ayarlar",
|
||||
"my_library": "Kütüphanem",
|
||||
"my_library": "Kütüphane",
|
||||
"downloading_metadata": "{{title}} (Meta verileri indiriliyor…)",
|
||||
"paused": "{{title}} (Durduruldu)",
|
||||
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
|
||||
@@ -36,15 +36,18 @@
|
||||
"downloads": "İndirilenler",
|
||||
"search_results": "Arama sonuçları",
|
||||
"settings": "Ayarlar",
|
||||
"version_available_install": "Sürüm {{version}} mevcut. Yüklemek ve yeniden başlatmak için buraya tıklayın.",
|
||||
"version_available_download": "Sürüm {{version}} mevcut. İndirmek için buraya tıklayın."
|
||||
"version_available_install": "{{version}} sürümü mevcut. Yüklemek ve yeniden başlatmak için buraya tıklayın.",
|
||||
"version_available_download": "{{version}} sürümü mevcut. İndirmek için buraya tıklayın."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Devam eden indirme yok",
|
||||
"downloading_metadata": "{{title}} meta verileri indiriliyor…",
|
||||
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Tamamlama: {{eta}} - Hız: {{speed}}",
|
||||
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Tamamlanma: {{eta}} - Hız: {{speed}}",
|
||||
"calculating_eta": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Kalan süre hesaplanıyor…",
|
||||
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)"
|
||||
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)",
|
||||
"installing_common_redist": "{{log}}…",
|
||||
"installation_complete": "İndirme tamamlandı",
|
||||
"installation_complete_message": "Genel bağımlılıklar başarıyla yüklendi."
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "Filtrele…",
|
||||
@@ -116,14 +119,14 @@
|
||||
"clear": "Temizle",
|
||||
"remove_files": "Dosyaları kaldır",
|
||||
"remove_from_library_title": "Emin misiniz?",
|
||||
"remove_from_library_description": "Bu işlem {{game}} oyununu kütüphanenizden kaldıracaktır",
|
||||
"remove_from_library_description": "Bu işlem sonrasında {{game}} oyunu kütüphanenizden kaldıracaktır",
|
||||
"options": "Seçenekler",
|
||||
"executable_section_title": "Çalıştırılabilir dosya",
|
||||
"executable_section_description": "\"Oyna\" tıklandığında çalıştırılacak dosyanın yolu",
|
||||
"executable_section_description": "\"Oyna\" butonuna tıklandığında çalıştırılacak dosyanın yolu",
|
||||
"downloads_secion_title": "İndirmeler",
|
||||
"downloads_section_description": "Bu oyun için güncellemeleri veya diğer sürümleri kontrol edin",
|
||||
"danger_zone_section_title": "Tehlike bölgesi",
|
||||
"danger_zone_section_description": "Bu oyunu kütüphanenizden veya Hydra tarafından indirilen dosyaları kaldırın",
|
||||
"danger_zone_section_description": "Bu oyunu kütüphanenizden kaldırın veya Hydra tarafından indirilen dosyaları silin.",
|
||||
"download_in_progress": "İndirme devam ediyor",
|
||||
"download_paused": "İndirme durduruldu",
|
||||
"last_downloaded_option": "Son indirilen seçenek",
|
||||
@@ -141,8 +144,8 @@
|
||||
"executable_path_in_use": "\"{{game}}\" tarafından kullanılan çalıştırılabilir dosya",
|
||||
"warning": "Uyarı:",
|
||||
"hydra_needs_to_remain_open": "Bu indirmenin tamamlanması için Hydra açık kalmalıdır. Eğer Hydra kapanırsa, ilerleme kaydedilmez.",
|
||||
"achievements": "Başarılar",
|
||||
"achievements_count": "Başarılar {{unlockedCount}}/{{achievementsCount}}",
|
||||
"achievements": "Başarımlar",
|
||||
"achievements_count": "Başarımlar {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "Bulut kaydı",
|
||||
"cloud_save_description": "İlerlemenizi buluta kaydedin ve herhangi bir cihazda oynamaya devam edin",
|
||||
"backups": "Yedekler",
|
||||
@@ -150,15 +153,15 @@
|
||||
"delete_backup": "Sil",
|
||||
"create_backup": "Yeni yedek oluştur",
|
||||
"last_backup_date": "{{date}} tarihindeki son yedek",
|
||||
"no_backup_preview": "Bu oyun için kayıtlı oyun bulunamadı",
|
||||
"no_backup_preview": "Bu oyun için bir kayıt dosyası bulunamadı",
|
||||
"restoring_backup": "Yedek geri yükleniyor ({{progress}} tamamlandı)…",
|
||||
"uploading_backup": "Yedek yükleniyor…",
|
||||
"no_backups": "Bu oyun için henüz bir yedek oluşturmadınız",
|
||||
"backup_uploaded": "Yedek yüklendi",
|
||||
"backup_deleted": "Yedek silindi",
|
||||
"backup_restored": "Yedek geri yüklendi",
|
||||
"see_all_achievements": "Tüm başarıları gör",
|
||||
"sign_in_to_see_achievements": "Başarıları görmek için giriş yapın",
|
||||
"see_all_achievements": "Tüm başarımları gör",
|
||||
"sign_in_to_see_achievements": "Başarımları görmek için oturum açın",
|
||||
"mapping_method_automatic": "Otomatik",
|
||||
"mapping_method_manual": "Manuel",
|
||||
"mapping_method_label": "Eşleme yöntemi",
|
||||
@@ -169,29 +172,38 @@
|
||||
"wine_prefix": "Wine Prefix",
|
||||
"wine_prefix_description": "Bu oyunu çalıştırmak için kullanılan Wine Prefix",
|
||||
"launch_options": "Başlatma Seçenekleri",
|
||||
"launch_options_description": "İleri düzey kullanıcılar, başlatma seçeneklerine değişiklikler girebilir (deneysel özellik)",
|
||||
"launch_options_description": "İleri düzey kullanıcılar, başlatma seçeneklerine parametreler girebilir (deneysel özellik)",
|
||||
"launch_options_placeholder": "Belirtilen bir parametre yok",
|
||||
"no_download_option_info": "Bilgi mevcut değil",
|
||||
"backup_deletion_failed": "Yedek silinemedi",
|
||||
"max_number_of_artifacts_reached": "Bu oyun için maksimum yedek sayısına ulaşıldı",
|
||||
"achievements_not_sync": "Başarılarınızı senkronize etmeyi öğrenin",
|
||||
"achievements_not_sync": "Başarımlarınızı senkronize etmeyi öğrenin",
|
||||
"manage_files_description": "Hangi dosyaların yedeklenip geri yükleneceğini yönetin",
|
||||
"select_folder": "Klasör seç",
|
||||
"backup_from": "{{date}} tarihinden yedek",
|
||||
"automatic_backup_from": "{{date}} tarihinden otomatik kayıt",
|
||||
"enable_automatic_cloud_sync": "Otomatik bulut kaydı senkronizasyonunu aktifleştir",
|
||||
"custom_backup_location_set": "Özel yedekleme konumu ayarlandı",
|
||||
"no_directory_selected": "Bir dizin seçilmedi",
|
||||
"no_write_permission": "Bu dizine indirme yapılamaz. Daha fazla bilgi için buraya tıklayın.",
|
||||
"reset_achievements": "Başarıları sıfırla",
|
||||
"reset_achievements_description": "Bu işlem {{game}} için tüm başarıları sıfırlar",
|
||||
"reset_achievements": "Başarımları sıfırla",
|
||||
"reset_achievements_description": "Bu işlem {{game}} için tüm başarımları sıfırlar",
|
||||
"reset_achievements_title": "Emin misiniz?",
|
||||
"reset_achievements_success": "Başarılar başarıyla sıfırlandı",
|
||||
"reset_achievements_error": "Başarılar sıfırlanamadı"
|
||||
"reset_achievements_success": "Başarımlar başarıyla sıfırlandı",
|
||||
"reset_achievements_error": "Başarımlar sıfırlanamadı",
|
||||
"download_error_gofile_quota_exceeded": "Gofile aylık kotanızı doldurdunuz. Kotanın yenilenmesini bekleyin.",
|
||||
"download_error_real_debrid_account_not_authorized": "Real-Debrid hesabınız yeni indirme işlemleri yapmak için yetkilendirilmemiş. Lütfen hesap ayarlarınızı kontrol edip tekrar deneyin.",
|
||||
"download_error_not_cached_in_real_debrid": "Bu indirme Real-Debrid üzerinde mevcut değil ve Real-Debrid'den indirme durumu henüz sorgulanamıyor.",
|
||||
"download_error_not_cached_in_torbox": "Bu indirme Torbox'ta mevcut değil ve Torbox'tan indirme durumu henüz sorgulanamıyor.",
|
||||
"game_removed_from_favorites": "Oyun favorilerden silindi",
|
||||
"game_added_to_favorites": "Oyun favorilere eklendi",
|
||||
"automatically_extract_downloaded_files": "Yüklenmiş dosyaları otomatik olarak çıkart"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Hydra'yı Aktive Et",
|
||||
"installation_id": "Kurulum Kimliği:",
|
||||
"enter_activation_code": "Aktivasyon kodunuzu girin",
|
||||
"message": "Bunu nereden soracağınızı bilmiyorsanız, bu sizin için olmamalı.",
|
||||
"message": "Bunu nasıl edineceğini bilmiyorsan, buna sahip olmamalısın.",
|
||||
"activate": "Aktive Et",
|
||||
"loading": "Yükleniyor…"
|
||||
},
|
||||
@@ -206,7 +218,7 @@
|
||||
"cancel": "İptal Et",
|
||||
"filter": "İndirilen oyunları filtrele",
|
||||
"remove": "Kaldır",
|
||||
"downloading_metadata": "Metadata indiriliyor…",
|
||||
"downloading_metadata": "Meta verileri indiriliyor…",
|
||||
"deleting": "Yükleyici siliniyor…",
|
||||
"delete": "Yükleyiciyi kaldır",
|
||||
"delete_modal_title": "Emin misiniz?",
|
||||
@@ -222,7 +234,9 @@
|
||||
"seeding": "Paylaşılıyor",
|
||||
"stop_seeding": "Paylaşımı durdur",
|
||||
"resume_seeding": "Paylaşımı sürdür",
|
||||
"options": "Yönet"
|
||||
"options": "Yönet",
|
||||
"extract": "Dosyaları çıkart",
|
||||
"extracting": "Dosyalar çıkartılıyor…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "İndirme yolu",
|
||||
@@ -231,7 +245,7 @@
|
||||
"enable_download_notifications": "Bir indirme tamamlandığında",
|
||||
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde",
|
||||
"real_debrid_api_token_label": "Real-Debrid API anahtarı",
|
||||
"quit_app_instead_hiding": "Hydra'yı kapatırken gizlemeyin",
|
||||
"quit_app_instead_hiding": "Hydra'yı kapatınca sistem tepsisine gitmesin",
|
||||
"launch_with_system": "Hydra'yı sistem başlatıldığında çalıştır",
|
||||
"general": "Genel",
|
||||
"behavior": "Davranış",
|
||||
@@ -250,6 +264,12 @@
|
||||
"validate_download_source": "Doğrula",
|
||||
"remove_download_source": "Kaldır",
|
||||
"add_download_source": "Kaynak ekle",
|
||||
"cancel_button_confirmation_delete_all_sources": "Hayır",
|
||||
"confirm_button_confirmation_delete_all_sources": "Evet, her şeyi sil",
|
||||
"description_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sileceksiniz",
|
||||
"title_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sil",
|
||||
"removed_download_sources": "Yazı tipleri kaldırıldı",
|
||||
"button_delete_all_sources": "Tüm indirme kaynaklarını kaldır",
|
||||
"download_count_zero": "İndirme seçeneği yok",
|
||||
"download_count_one": "{{countFormatted}} indirme seçeneği",
|
||||
"download_count_other": "{{countFormatted}} indirme seçeneği",
|
||||
@@ -277,11 +297,64 @@
|
||||
"must_be_valid_url": "Kaynak geçerli bir URL olmalıdır",
|
||||
"blocked_users": "Engellenen kullanıcılar",
|
||||
"user_unblocked": "Kullanıcının engeli kaldırıldı",
|
||||
"enable_achievement_notifications": "Bir başarı kilidi açıldığında",
|
||||
"enable_achievement_notifications": "Bir başarım kilidi açıldığında",
|
||||
"launch_minimized": "Hydra'yı küçültülmüş başlat",
|
||||
"disable_nsfw_alert": "NSFW uyarısını devre dışı bırak",
|
||||
"seed_after_download_complete": "İndirme tamamlandıktan sonra paylaş",
|
||||
"show_hidden_achievement_description": "Gizli başarı açıklamalarını kilitlenmeden önce göster"
|
||||
"show_hidden_achievement_description": "Gizli başarım açıklamalarını kilitlenmeden önce göster",
|
||||
"account": "Hesap",
|
||||
"no_users_blocked": "Hiçbir kullanıcıyı engellemediniz",
|
||||
"subscription_active_until": "Hydra Cloud'unuz {{date}} tarihine kadar aktif",
|
||||
"manage_subscription": "Aboneliği yönet",
|
||||
"update_email": "E-posta'yı güncelle",
|
||||
"update_password": "Şifreyi güncelle",
|
||||
"current_email": "Aktif e-posta'nız",
|
||||
"no_email_account": "Henüz ayarlanmış bir e-postanız yok",
|
||||
"account_data_updated_successfully": "Hesap bilgileri başarıyla güncellendi",
|
||||
"renew_subscription": "Hydra Cloud'u yenile",
|
||||
"subscription_expired_at": "Aboneliğiniz {{date}} tarihinde sona erdi",
|
||||
"no_subscription": "Hydra'yı en iyi şekilde deneyimleyin",
|
||||
"become_subscriber": "Hydra Cloud'lu ol",
|
||||
"subscription_renew_cancelled": "Otomatik yenileme devre dışı",
|
||||
"subscription_renews_on": "Aboneliğiniz {{date}} tarihinde yenilenecek",
|
||||
"bill_sent_until": "Bir sonraki faturanız bu tarihe kadar gönderilecek",
|
||||
"no_themes": "Henüz bir temanız yok gibi görünüyor, ama endişelenmeyin, ilk şaheserinizi oluşturmak için buraya tıklayın.",
|
||||
"editor_tab_code": "Kod",
|
||||
"editor_tab_info": "Bilgi",
|
||||
"editor_tab_save": "Kaydet",
|
||||
"web_store": "İnternet mağazası",
|
||||
"clear_themes": "Temizle",
|
||||
"create_theme": "Oluştur",
|
||||
"create_theme_modal_title": "Tema oluştur",
|
||||
"create_theme_modal_description": "Hydra'nın görünümünü özelleştirmek için yeni bir tema oluştur",
|
||||
"theme_name": "İsim",
|
||||
"insert_theme_name": "Tema ismini gir",
|
||||
"set_theme": "Temayı seç",
|
||||
"unset_theme": "Tema seçimini kaldır",
|
||||
"delete_theme": "Temayı sil",
|
||||
"edit_theme": "Temayı düzenle",
|
||||
"delete_all_themes": "Tüm temaları sil",
|
||||
"delete_all_themes_description": "Bu tüm temalarınızı silecektir",
|
||||
"delete_theme_description": "Bu {{theme}} temasını silecektir",
|
||||
"cancel": "İptal",
|
||||
"appearance": "Görünüm",
|
||||
"enable_torbox": "Torbox'u etkinleştir",
|
||||
"torbox_description": "TorBox, piyasadaki en iyi sunucularla bile rekabet edebilen premium seedbox hizmetinizdir.",
|
||||
"torbox_account_linked": "TorBox hesabı bağlandı",
|
||||
"create_real_debrid_account": "Henüz bir Real-Debrid hesabınız yoksa buraya tıklayın",
|
||||
"create_torbox_account": "Henüz bir TorBox hesabınız yoksa buraya tıklayın",
|
||||
"real_debrid_account_linked": "Real-Debrid hesabı bağlandı",
|
||||
"name_min_length": "Tema ismi en az 3 karakter uzunluğunda olmalıdır",
|
||||
"import_theme": "Temayı içe aktar",
|
||||
"import_theme_description": "{{theme}} teması, tema mağazasından içeri aktarılacak",
|
||||
"error_importing_theme": "Temayı içe aktarmada bir sorun oluştu",
|
||||
"theme_imported": "Tema başarıyla içe aktarıldı",
|
||||
"enable_friend_request_notifications": "Bir arkadaşlık isteği alındığında",
|
||||
"enable_auto_install": "Güncellemeleri otomatik yükle",
|
||||
"common_redist": "Ortak bağımlılıklar",
|
||||
"common_redist_description": "Bazı oyunların çalışabilmesi için genel bağımlılıklar gereklidir. Sorun yaşamamak için bunların yüklenmesi önerilir.",
|
||||
"install_common_redist": "Yükle",
|
||||
"installing_common_redist": "Yükleniyor…"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "İndirme tamamlandı",
|
||||
@@ -289,17 +362,23 @@
|
||||
"repack_list_updated": "Repack listesi güncellendi",
|
||||
"repack_count_one": "{{count}} repack eklendi",
|
||||
"repack_count_other": "{{count}} repack eklendi",
|
||||
"new_update_available": "Sürüm {{version}} mevcut",
|
||||
"new_update_available": "{{version}} sürümü mevcut",
|
||||
"restart_to_install_update": "Güncellemeyi yüklemek için Hydra'yı yeniden başlatın",
|
||||
"notification_achievement_unlocked_title": "{{game}} için başarı kilidi açıldı",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarılar açıldı"
|
||||
"notification_achievement_unlocked_title": "{{game}} için başarım kilidi açıldı",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarım açıldı",
|
||||
"new_friend_request_description": "Yeni bir arkadaşlık isteğin var",
|
||||
"new_friend_request_title": "Yeni arkadaşlık isteği",
|
||||
"extraction_complete": "Çıkartma tamamlandı",
|
||||
"game_extracted": "{{title}} başarıyla çıkartıldı"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Hydra'yı Aç",
|
||||
"quit": "Çık"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "İndirilebilir içerik bulunmuyor"
|
||||
"no_downloads": "İndirilebilir içerik bulunmuyor",
|
||||
"available_one": "Mevcut",
|
||||
"available_other": "Mevcut"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Programlar Yüklü Değil",
|
||||
@@ -332,7 +411,7 @@
|
||||
"successfully_signed_out": "Başarıyla çıkış yapıldı",
|
||||
"sign_out": "Çıkış yap",
|
||||
"playing_for": "{{amount}} oynanıyor",
|
||||
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Çıkış yaptığınızda kütüphaneniz görünür olmayacak ve herhangi bir ilerleme kaydedilmeyecek. Çıkışa devam etmek istiyor musunuz?",
|
||||
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Oturumu kapattığınızda kütüphaneniz görünür olmayacak ve herhangi bir ilerleme kaydedilmeyecek. Oturumu kapatmaya devam etmek istiyor musunuz?",
|
||||
"add_friends": "Arkadaş Ekle",
|
||||
"add": "Ekle",
|
||||
"friend_code": "Arkadaş kodu",
|
||||
@@ -383,39 +462,39 @@
|
||||
"uploading_banner": "Afiş yükleniyor…",
|
||||
"background_image_updated": "Arka plan görüntüsü güncellendi",
|
||||
"stats": "İstatistikler",
|
||||
"achievements": "Başarılar",
|
||||
"achievements": "Başarımlar",
|
||||
"games": "Oyunlar",
|
||||
"top_percentile": "En üst {{percentile}}%",
|
||||
"ranking_updated_weekly": "Sıralama haftalık olarak güncellenir",
|
||||
"playing": "{{game}} oynanıyor",
|
||||
"achievements_unlocked": "Başarılar açıldı",
|
||||
"achievements_unlocked": "Başarımlar açıldı",
|
||||
"earned_points": "Kazanılan puanlar",
|
||||
"show_achievements_on_profile": "Başarılarınızı profilinizde gösterin",
|
||||
"show_achievements_on_profile": "Başarımlarınızı profilinizde gösterin",
|
||||
"show_points_on_profile": "Kazandığınız puanları profilinizde gösterin"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Başarı açıldı",
|
||||
"user_achievements": "{{displayName}}'in Başarıları",
|
||||
"your_achievements": "Başarılarınız",
|
||||
"achievement_unlocked": "Başarım açıldı",
|
||||
"user_achievements": "{{displayName}} oyununun Başarımları",
|
||||
"your_achievements": "Başarımlarınız",
|
||||
"unlocked_at": "Açılma zamanı: {{date}}",
|
||||
"subscription_needed": "Bu içeriği görmek için bir Hydra Cloud aboneliği gereklidir",
|
||||
"new_achievements_unlocked": "{{gameCount}} oyundan {{achievementCount}} yeni başarı açıldı",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarı",
|
||||
"achievements_unlocked_for_game": "{{gameTitle}} oyunu için {{achievementCount}} yeni başarı açıldı",
|
||||
"hidden_achievement_tooltip": "Bu gizli bir başarıdır",
|
||||
"achievement_earn_points": "Bu başarı ile {{points}} puan kazanın",
|
||||
"new_achievements_unlocked": "{{gameCount}} oyundan {{achievementCount}} yeni başarım açıldı",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarım",
|
||||
"achievements_unlocked_for_game": "{{gameTitle}} oyunu için {{achievementCount}} yeni başarım açıldı",
|
||||
"hidden_achievement_tooltip": "Bu gizli bir başarımdır",
|
||||
"achievement_earn_points": "Bu başarım ile {{points}} puan kazanın",
|
||||
"earned_points": "Kazanılan puanlar:",
|
||||
"available_points": "Mevcut puanlar:",
|
||||
"how_to_earn_achievements_points": "Başarı puanları nasıl kazanılır?"
|
||||
"how_to_earn_achievements_points": "Başarım puanları nasıl kazanılır?"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "Hydra Cloud Aboneliği",
|
||||
"subscribe_now": "Şimdi abone olun",
|
||||
"cloud_saving": "Bulut kaydetme",
|
||||
"cloud_achievements": "Başarılarınızı buluta kaydedin",
|
||||
"cloud_achievements": "Başarımlarınızı buluta kaydedin",
|
||||
"animated_profile_picture": "Animasyonlu profil resimleri",
|
||||
"premium_support": "Premium Destek",
|
||||
"show_and_compare_achievements": "Başarılarınızı diğer kullanıcılarla karşılaştırın ve gösterin",
|
||||
"show_and_compare_achievements": "Başarımlarınızı diğer kullanıcılarla karşılaştırın ve gösterin",
|
||||
"animated_profile_banner": "Animasyonlu profil afişi",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "Bir Hydra Cloud özelliği keşfettiniz!",
|
||||
|
||||
@@ -176,6 +176,12 @@
|
||||
"import": "Імпортувати",
|
||||
"insert_valid_json_url": "Вставте дійсний URL JSON-файлу",
|
||||
"language": "Мова",
|
||||
"cancel_button_confirmation_delete_all_sources": "немає",
|
||||
"confirm_button_confirmation_delete_all_sources": "Так, видалити все",
|
||||
"description_confirmation_delete_all_sources": "Ви видалите всі джерела завантаження",
|
||||
"title_confirmation_delete_all_sources": "Видалити всі джерела завантаження",
|
||||
"removed_download_sources": "Джерела завантажень видалено",
|
||||
"button_delete_all_sources": "Видаліть усі джерела завантаження",
|
||||
"api_token": "API-токен",
|
||||
"debrid_api_token_hint": "API токен можливо отримати <0>тут</0>",
|
||||
"real_debrid_api_token_label": "Real-Debrid API-токен",
|
||||
|
||||
@@ -226,6 +226,12 @@
|
||||
"download_sources_description": "Hydra 将从这些源获取下载链接。源 URL 必须是直接链接到包含下载链接的 .json 文件。",
|
||||
"validate_download_source": "验证",
|
||||
"remove_download_source": "移除",
|
||||
"cancel_button_confirmation_delete_all_sources": "不",
|
||||
"confirm_button_confirmation_delete_all_sources": "是的,删除所有内容",
|
||||
"description_confirmation_delete_all_sources": "您将删除所有下载源",
|
||||
"title_confirmation_delete_all_sources": "删除所有下载源",
|
||||
"removed_download_sources": "已删除字体",
|
||||
"button_delete_all_sources": "删除所有下载源",
|
||||
"add_download_source": "添加源",
|
||||
"download_count_zero": "列表中无下载",
|
||||
"download_count_one": "列表中有 {{countFormatted}} 个下载",
|
||||
|
||||
@@ -12,10 +12,9 @@ export const levelDatabasePath = path.join(
|
||||
`hydra-db${isStaging ? "-staging" : ""}`
|
||||
);
|
||||
|
||||
export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
|
||||
export const databasePath = path.join(
|
||||
databaseDirectory,
|
||||
isStaging ? "hydra_test.db" : "hydra.db"
|
||||
export const commonRedistPath = path.join(
|
||||
app.getPath("userData"),
|
||||
"CommonRedist"
|
||||
);
|
||||
|
||||
export const logsPath = path.join(app.getPath("userData"), "logs");
|
||||
@@ -31,3 +30,5 @@ export const achievementSoundPath = app.isPackaged
|
||||
export const backupsPath = path.join(app.getPath("userData"), "Backups");
|
||||
|
||||
export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
|
||||
|
||||
export const MAIN_LOOP_INTERVAL = 1500;
|
||||
|
||||
@@ -4,11 +4,15 @@ import updater from "electron-updater";
|
||||
|
||||
const { autoUpdater } = updater;
|
||||
|
||||
const restartAndInstallUpdate = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
export const restartAndInstallUpdate = () => {
|
||||
autoUpdater.removeAllListeners();
|
||||
if (app.isPackaged) {
|
||||
autoUpdater.quitAndInstall(false);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("restartAndInstallUpdate", restartAndInstallUpdate);
|
||||
const restartAndInstallUpdateEvent = async (
|
||||
_event: Electron.IpcMainInvokeEvent
|
||||
) => restartAndInstallUpdate();
|
||||
|
||||
registerEvent("restartAndInstallUpdate", restartAndInstallUpdateEvent);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { CloudSync } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
import type { GameShop } from "@types";
|
||||
import { t } from "i18next";
|
||||
import { format } from "date-fns";
|
||||
import i18next, { t } from "i18next";
|
||||
import { formatDate } from "date-fns";
|
||||
|
||||
const uploadSaveGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -10,13 +10,15 @@ const uploadSaveGame = async (
|
||||
shop: GameShop,
|
||||
downloadOptionTitle: string | null
|
||||
) => {
|
||||
const { language } = i18next;
|
||||
|
||||
return CloudSync.uploadSaveGame(
|
||||
objectId,
|
||||
shop,
|
||||
downloadOptionTitle,
|
||||
t("backup_from", {
|
||||
ns: "game_details",
|
||||
date: format(new Date(), "dd/MM/yyyy"),
|
||||
date: formatDate(new Date(), language),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
13
src/main/events/download-sources/create-download-sources.ts
Normal file
13
src/main/events/download-sources/create-download-sources.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { HydraApi } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const createDownloadSources = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
urls: string[]
|
||||
) => {
|
||||
await HydraApi.post("/profile/download-sources", {
|
||||
urls,
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("createDownloadSources", createDownloadSources);
|
||||
8
src/main/events/download-sources/get-download-sources.ts
Normal file
8
src/main/events/download-sources/get-download-sources.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { HydraApi } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return HydraApi.get("/profile/download-sources");
|
||||
};
|
||||
|
||||
registerEvent("getDownloadSources", getDownloadSources);
|
||||
18
src/main/events/download-sources/remove-download-source.ts
Normal file
18
src/main/events/download-sources/remove-download-source.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { HydraApi } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const removeDownloadSource = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
url?: string,
|
||||
removeAll = false
|
||||
) => {
|
||||
const params = new URLSearchParams({
|
||||
all: removeAll.toString(),
|
||||
});
|
||||
|
||||
if (url) params.set("url", url);
|
||||
|
||||
return HydraApi.delete(`/profile/download-sources?${params.toString()}`);
|
||||
};
|
||||
|
||||
registerEvent("removeDownloadSource", removeDownloadSource);
|
||||
@@ -20,6 +20,7 @@ import "./library/close-game";
|
||||
import "./library/delete-game-folder";
|
||||
import "./library/get-game-by-object-id";
|
||||
import "./library/get-library";
|
||||
import "./library/extract-game-download";
|
||||
import "./library/open-game";
|
||||
import "./library/open-game-executable-path";
|
||||
import "./library/open-game-installer";
|
||||
@@ -38,12 +39,15 @@ import "./misc/show-open-dialog";
|
||||
import "./misc/get-features";
|
||||
import "./misc/show-item-in-folder";
|
||||
import "./misc/get-badges";
|
||||
import "./misc/install-common-redist";
|
||||
import "./misc/can-install-common-redist";
|
||||
import "./torrenting/cancel-game-download";
|
||||
import "./torrenting/pause-game-download";
|
||||
import "./torrenting/resume-game-download";
|
||||
import "./torrenting/start-game-download";
|
||||
import "./torrenting/pause-game-seed";
|
||||
import "./torrenting/resume-game-seed";
|
||||
import "./torrenting/check-debrid-availability";
|
||||
import "./user-preferences/get-user-preferences";
|
||||
import "./user-preferences/update-user-preferences";
|
||||
import "./user-preferences/auto-launch";
|
||||
@@ -90,6 +94,9 @@ import "./themes/get-custom-theme-by-id";
|
||||
import "./themes/get-active-custom-theme";
|
||||
import "./themes/close-editor-window";
|
||||
import "./themes/toggle-custom-theme";
|
||||
import "./download-sources/create-download-sources";
|
||||
import "./download-sources/remove-download-source";
|
||||
import "./download-sources/get-download-sources";
|
||||
import { isPortableVersion } from "@main/helpers";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
|
||||
@@ -13,35 +13,42 @@ const deleteGameFolder = async (
|
||||
objectId: string
|
||||
): Promise<void> => {
|
||||
const downloadKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const download = await downloadsSublevel.get(downloadKey);
|
||||
|
||||
if (!download) return;
|
||||
if (!download?.folderName) return;
|
||||
|
||||
if (download.folderName) {
|
||||
const folderPath = path.join(
|
||||
download.downloadPath ?? (await getDownloadsPath()),
|
||||
download.folderName
|
||||
);
|
||||
const folderPath = path.join(
|
||||
download.downloadPath ?? (await getDownloadsPath()),
|
||||
download.folderName
|
||||
);
|
||||
|
||||
if (fs.existsSync(folderPath)) {
|
||||
const metaPath = `${folderPath}.meta`;
|
||||
|
||||
const deleteFile = async (filePath: string, isDirectory = false) => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
fs.rm(
|
||||
folderPath,
|
||||
{ recursive: true, force: true, maxRetries: 5, retryDelay: 200 },
|
||||
filePath,
|
||||
{
|
||||
recursive: isDirectory,
|
||||
force: true,
|
||||
maxRetries: 5,
|
||||
retryDelay: 200,
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
logger.error(error);
|
||||
reject();
|
||||
}
|
||||
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await deleteFile(folderPath, true);
|
||||
await deleteFile(metaPath);
|
||||
await downloadsSublevel.del(downloadKey);
|
||||
};
|
||||
|
||||
|
||||
46
src/main/events/library/extract-game-download.ts
Normal file
46
src/main/events/library/extract-game-download.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { GameShop } from "@types";
|
||||
import path from "node:path";
|
||||
import { GameFilesManager } from "@main/services";
|
||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
|
||||
|
||||
const extractGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
): Promise<boolean> => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const [download, game] = await Promise.all([
|
||||
downloadsSublevel.get(gameKey),
|
||||
gamesSublevel.get(gameKey),
|
||||
]);
|
||||
|
||||
if (!download || !game) return false;
|
||||
|
||||
await downloadsSublevel.put(gameKey, {
|
||||
...download,
|
||||
extracting: true,
|
||||
});
|
||||
|
||||
const gameFilesManager = new GameFilesManager(shop, objectId);
|
||||
|
||||
if (
|
||||
FILE_EXTENSIONS_TO_EXTRACT.some((ext) => download.folderName?.endsWith(ext))
|
||||
) {
|
||||
gameFilesManager.extractDownloadedFile();
|
||||
} else {
|
||||
gameFilesManager
|
||||
.extractFilesInDirectory(
|
||||
path.join(download.downloadPath, download.folderName!)
|
||||
)
|
||||
.then(() => {
|
||||
gameFilesManager.setExtractionComplete(false);
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
registerEvent("extractGameDownload", extractGameDownload);
|
||||
@@ -21,6 +21,8 @@ const updateExecutablePath = async (
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
executablePath: parsedPath,
|
||||
automaticCloudSync:
|
||||
executablePath === null ? false : game.automaticCloudSync,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
7
src/main/events/misc/can-install-common-redist.ts
Normal file
7
src/main/events/misc/can-install-common-redist.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { CommonRedistManager } from "@main/services/common-redist-manager";
|
||||
|
||||
const canInstallCommonRedist = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||
CommonRedistManager.canInstallCommonRedist();
|
||||
|
||||
registerEvent("canInstallCommonRedist", canInstallCommonRedist);
|
||||
10
src/main/events/misc/install-common-redist.ts
Normal file
10
src/main/events/misc/install-common-redist.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { CommonRedistManager } from "@main/services/common-redist-manager";
|
||||
|
||||
const installCommonRedist = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
if (await CommonRedistManager.canInstallCommonRedist()) {
|
||||
CommonRedistManager.installCommonRedist();
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("installCommonRedist", installCommonRedist);
|
||||
@@ -1,17 +1,59 @@
|
||||
import { MAIN_LOOP_INTERVAL } from "@main/constants";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { HydraApi, WindowManager } from "@main/services";
|
||||
import { publishNewFriendRequestNotification } from "@main/services/notifications";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
import type { FriendRequestSync } from "@types";
|
||||
|
||||
const syncFriendRequests = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`).catch(
|
||||
(err) => {
|
||||
if (err instanceof UserNotLoggedInError) {
|
||||
return { friendRequests: [] };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
interface SyncState {
|
||||
friendRequestCount: number | null;
|
||||
tick: number;
|
||||
}
|
||||
|
||||
const ticksToUpdate = (2 * 60 * 1000) / MAIN_LOOP_INTERVAL; // 2 minutes
|
||||
|
||||
const syncState: SyncState = {
|
||||
friendRequestCount: null,
|
||||
tick: 0,
|
||||
};
|
||||
|
||||
registerEvent("syncFriendRequests", syncFriendRequests);
|
||||
const syncFriendRequests = async () => {
|
||||
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`)
|
||||
.then((res) => {
|
||||
if (
|
||||
syncState.friendRequestCount != null &&
|
||||
syncState.friendRequestCount < res.friendRequestCount
|
||||
) {
|
||||
publishNewFriendRequestNotification();
|
||||
}
|
||||
|
||||
syncState.friendRequestCount = res.friendRequestCount;
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
"on-sync-friend-requests",
|
||||
res
|
||||
);
|
||||
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err instanceof UserNotLoggedInError) {
|
||||
return { friendRequestCount: 0 } as FriendRequestSync;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
const syncFriendRequestsEvent = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return syncFriendRequests();
|
||||
};
|
||||
|
||||
export const watchFriendRequests = async () => {
|
||||
if (syncState.tick % ticksToUpdate === 0) {
|
||||
await syncFriendRequests();
|
||||
}
|
||||
|
||||
syncState.tick++;
|
||||
};
|
||||
|
||||
registerEvent("syncFriendRequests", syncFriendRequestsEvent);
|
||||
|
||||
11
src/main/events/torrenting/check-debrid-availability.ts
Normal file
11
src/main/events/torrenting/check-debrid-availability.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { HydraDebridClient } from "@main/services/download/hydra-debrid";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const checkDebridAvailability = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
magnets: string[]
|
||||
) => {
|
||||
return HydraDebridClient.getAvailableMagnets(magnets);
|
||||
};
|
||||
|
||||
registerEvent("checkDebridAvailability", checkDebridAvailability);
|
||||
@@ -12,7 +12,15 @@ const startGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
payload: StartGameDownloadPayload
|
||||
) => {
|
||||
const { objectId, title, shop, downloadPath, downloader, uri } = payload;
|
||||
const {
|
||||
objectId,
|
||||
title,
|
||||
shop,
|
||||
downloadPath,
|
||||
downloader,
|
||||
uri,
|
||||
automaticallyExtract,
|
||||
} = payload;
|
||||
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
@@ -74,6 +82,8 @@ const startGameDownload = async (
|
||||
shouldSeed: false,
|
||||
timestamp: Date.now(),
|
||||
queued: true,
|
||||
extracting: false,
|
||||
automaticallyExtract,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -23,10 +23,6 @@ const updateUserPreferences = async (
|
||||
patchUserProfile({ language: preferences.language }).catch(() => {});
|
||||
}
|
||||
|
||||
if (!preferences.downloadsPath) {
|
||||
preferences.downloadsPath = null;
|
||||
}
|
||||
|
||||
await db.put<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
|
||||
@@ -8,7 +8,6 @@ import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { logger, WindowManager } from "@main/services";
|
||||
import resources from "@locales";
|
||||
import { PythonRPC } from "./services/python-rpc";
|
||||
import { Aria2 } from "./services/aria2";
|
||||
import { db, levelKeys } from "./level";
|
||||
import { loadState } from "./main";
|
||||
|
||||
@@ -143,7 +142,6 @@ app.on("window-all-closed", () => {
|
||||
app.on("before-quit", () => {
|
||||
/* Disconnects libtorrent */
|
||||
PythonRPC.kill();
|
||||
Aria2.kill();
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import knex from "knex";
|
||||
import { databasePath } from "./constants";
|
||||
import { app } from "electron";
|
||||
|
||||
export const knexClient = knex({
|
||||
debug: !app.isPackaged,
|
||||
client: "better-sqlite3",
|
||||
connection: {
|
||||
filename: databasePath,
|
||||
},
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
const config = {
|
||||
development: {
|
||||
migrations: {
|
||||
extension: "ts",
|
||||
stub: "migrations/migration.stub",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -13,5 +13,5 @@ export const levelKeys = {
|
||||
downloads: "downloads",
|
||||
userPreferences: "userPreferences",
|
||||
language: "language",
|
||||
sqliteMigrationDone: "sqliteMigrationDone",
|
||||
screenState: "screenState",
|
||||
};
|
||||
|
||||
174
src/main/main.ts
174
src/main/main.ts
@@ -1,36 +1,25 @@
|
||||
import { DownloadManager, logger, Ludusavi, startMainLoop } from "./services";
|
||||
import { DownloadManager, Ludusavi, startMainLoop } from "./services";
|
||||
import { RealDebridClient } from "./services/download/real-debrid";
|
||||
import { HydraApi } from "./services/hydra-api";
|
||||
import { uploadGamesBatch } from "./services/library-sync";
|
||||
import { Aria2 } from "./services/aria2";
|
||||
import { downloadsSublevel } from "./level/sublevels/downloads";
|
||||
import { sortBy } from "lodash-es";
|
||||
import { Downloader } from "@shared";
|
||||
import {
|
||||
gameAchievementsSublevel,
|
||||
gamesSublevel,
|
||||
levelKeys,
|
||||
db,
|
||||
} from "./level";
|
||||
import { Auth, User, type UserPreferences } from "@types";
|
||||
import { knexClient } from "./knex-client";
|
||||
import { levelKeys, db } from "./level";
|
||||
import type { UserPreferences } from "@types";
|
||||
import { TorBoxClient } from "./services/download/torbox";
|
||||
import { CommonRedistManager } from "./services/common-redist-manager";
|
||||
|
||||
export const loadState = async () => {
|
||||
const userPreferences = await migrateFromSqlite().then(async () => {
|
||||
await db.put<string, boolean>(levelKeys.sqliteMigrationDone, true, {
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
});
|
||||
|
||||
return db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
await import("./events");
|
||||
|
||||
Aria2.spawn();
|
||||
|
||||
if (userPreferences?.realDebridApiToken) {
|
||||
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||
}
|
||||
@@ -52,6 +41,15 @@ export const loadState = async () => {
|
||||
return sortBy(games, "timestamp", "DESC");
|
||||
});
|
||||
|
||||
downloads.forEach((download) => {
|
||||
if (download.extracting) {
|
||||
downloadsSublevel.put(levelKeys.game(download.shop, download.objectId), {
|
||||
...download,
|
||||
extracting: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const [nextItemOnQueue] = downloads.filter((game) => game.queued);
|
||||
|
||||
const downloadsToSeed = downloads.filter(
|
||||
@@ -65,138 +63,6 @@ export const loadState = async () => {
|
||||
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
|
||||
|
||||
startMainLoop();
|
||||
};
|
||||
|
||||
const migrateFromSqlite = async () => {
|
||||
const sqliteMigrationDone = await db.get(levelKeys.sqliteMigrationDone);
|
||||
|
||||
if (sqliteMigrationDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
const migrateGames = knexClient("game")
|
||||
.select("*")
|
||||
.then((games) => {
|
||||
return gamesSublevel.batch(
|
||||
games.map((game) => ({
|
||||
type: "put",
|
||||
key: levelKeys.game(game.shop, game.objectID),
|
||||
value: {
|
||||
objectId: game.objectID,
|
||||
shop: game.shop,
|
||||
title: game.title,
|
||||
iconUrl: game.iconUrl,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
remoteId: game.remoteId,
|
||||
winePrefixPath: game.winePrefixPath,
|
||||
launchOptions: game.launchOptions,
|
||||
executablePath: game.executablePath,
|
||||
isDeleted: game.isDeleted === 1,
|
||||
},
|
||||
}))
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("Games migrated successfully");
|
||||
});
|
||||
|
||||
const migrateUserPreferences = knexClient("user_preferences")
|
||||
.select("*")
|
||||
.then(async (userPreferences) => {
|
||||
if (userPreferences.length > 0) {
|
||||
const { realDebridApiToken, ...rest } = userPreferences[0];
|
||||
|
||||
await db.put<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
...rest,
|
||||
realDebridApiToken,
|
||||
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
||||
runAtStartup: rest.runAtStartup === 1,
|
||||
startMinimized: rest.startMinimized === 1,
|
||||
disableNsfwAlert: rest.disableNsfwAlert === 1,
|
||||
seedAfterDownloadComplete: rest.seedAfterDownloadComplete === 1,
|
||||
showHiddenAchievementsDescription:
|
||||
rest.showHiddenAchievementsDescription === 1,
|
||||
downloadNotificationsEnabled:
|
||||
rest.downloadNotificationsEnabled === 1,
|
||||
repackUpdatesNotificationsEnabled:
|
||||
rest.repackUpdatesNotificationsEnabled === 1,
|
||||
achievementNotificationsEnabled:
|
||||
rest.achievementNotificationsEnabled === 1,
|
||||
},
|
||||
{ valueEncoding: "json" }
|
||||
);
|
||||
|
||||
if (rest.language) {
|
||||
await db.put<string, string>(levelKeys.language, rest.language, {
|
||||
valueEncoding: "utf-8",
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("User preferences migrated successfully");
|
||||
});
|
||||
|
||||
const migrateAchievements = knexClient("game_achievement")
|
||||
.select("*")
|
||||
.then((achievements) => {
|
||||
return gameAchievementsSublevel.batch(
|
||||
achievements.map((achievement) => ({
|
||||
type: "put",
|
||||
key: levelKeys.game(achievement.shop, achievement.objectId),
|
||||
value: {
|
||||
achievements: JSON.parse(achievement.achievements),
|
||||
unlockedAchievements: JSON.parse(achievement.unlockedAchievements),
|
||||
},
|
||||
}))
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("Achievements migrated successfully");
|
||||
});
|
||||
|
||||
const migrateUser = knexClient("user_auth")
|
||||
.select("*")
|
||||
.then(async (users) => {
|
||||
if (users.length > 0) {
|
||||
await db.put<string, User>(
|
||||
levelKeys.user,
|
||||
{
|
||||
id: users[0].userId,
|
||||
displayName: users[0].displayName,
|
||||
profileImageUrl: users[0].profileImageUrl,
|
||||
backgroundImageUrl: users[0].backgroundImageUrl,
|
||||
subscription: users[0].subscription,
|
||||
},
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
await db.put<string, Auth>(
|
||||
levelKeys.auth,
|
||||
{
|
||||
accessToken: users[0].accessToken,
|
||||
refreshToken: users[0].refreshToken,
|
||||
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
|
||||
},
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("User data migrated successfully");
|
||||
});
|
||||
|
||||
return Promise.allSettled([
|
||||
migrateGames,
|
||||
migrateUserPreferences,
|
||||
migrateAchievements,
|
||||
migrateUser,
|
||||
]);
|
||||
|
||||
CommonRedistManager.downloadCommonRedist();
|
||||
};
|
||||
|
||||
74
src/main/services/7zip.ts
Normal file
74
src/main/services/7zip.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { app } from "electron";
|
||||
import cp from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export const binaryName = {
|
||||
linux: "7zzs",
|
||||
darwin: "7zz",
|
||||
win32: "7z.exe",
|
||||
};
|
||||
|
||||
export class SevenZip {
|
||||
private static readonly binaryPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, binaryName[process.platform])
|
||||
: path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"binaries",
|
||||
binaryName[process.platform]
|
||||
);
|
||||
|
||||
public static extractFile(
|
||||
{
|
||||
filePath,
|
||||
outputPath,
|
||||
cwd,
|
||||
passwords = [],
|
||||
}: {
|
||||
filePath: string;
|
||||
outputPath?: string;
|
||||
cwd?: string;
|
||||
passwords?: string[];
|
||||
},
|
||||
successCb: () => void,
|
||||
errorCb: () => void
|
||||
) {
|
||||
const tryPassword = (index = -1) => {
|
||||
const password = passwords[index] ?? "";
|
||||
logger.info(`Trying password ${password} on ${filePath}`);
|
||||
|
||||
const args = ["x", filePath, "-y", "-p" + password];
|
||||
|
||||
if (outputPath) {
|
||||
args.push("-o" + outputPath);
|
||||
}
|
||||
|
||||
const child = cp.execFile(this.binaryPath, args, {
|
||||
cwd,
|
||||
});
|
||||
|
||||
child.once("exit", (code) => {
|
||||
if (code === 0) {
|
||||
successCb();
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < passwords.length - 1) {
|
||||
logger.info(
|
||||
`Failed to extract file: ${filePath} with password: ${password}. Trying next password...`
|
||||
);
|
||||
|
||||
tryPassword(index + 1);
|
||||
} else {
|
||||
logger.info(`Failed to extract file: ${filePath}`);
|
||||
|
||||
errorCb();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tryPassword();
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,13 @@ export const getGameAchievementData = async (
|
||||
if (cachedAchievements && useCachedData)
|
||||
return cachedAchievements.achievements;
|
||||
|
||||
if (
|
||||
cachedAchievements &&
|
||||
Date.now() < (cachedAchievements.cacheExpiresTimestamp ?? 0)
|
||||
) {
|
||||
return cachedAchievements.achievements;
|
||||
}
|
||||
|
||||
const language = await db
|
||||
.get<string, string>(levelKeys.language, {
|
||||
valueEncoding: "utf-8",
|
||||
@@ -31,6 +38,7 @@ export const getGameAchievementData = async (
|
||||
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
|
||||
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
|
||||
achievements,
|
||||
cacheExpiresTimestamp: Date.now() + 1000 * 60 * 30, // 30 minutes
|
||||
});
|
||||
|
||||
return achievements;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
Game,
|
||||
GameShop,
|
||||
UnlockedAchievement,
|
||||
UpdatedUnlockedAchievements,
|
||||
UserPreferences,
|
||||
} from "@types";
|
||||
import { WindowManager } from "../window-manager";
|
||||
@@ -26,6 +27,7 @@ const saveAchievementsOnLocal = async (
|
||||
await gameAchievementsSublevel.put(levelKey, {
|
||||
achievements: gameAchievement?.achievements ?? [],
|
||||
unlockedAchievements: unlockedAchievements,
|
||||
cacheExpiresTimestamp: gameAchievement?.cacheExpiresTimestamp,
|
||||
});
|
||||
|
||||
if (!sendUpdateEvent) return;
|
||||
@@ -114,7 +116,7 @@ export const mergeAchievements = async (
|
||||
}
|
||||
|
||||
if (game.remoteId) {
|
||||
await HydraApi.put(
|
||||
await HydraApi.put<UpdatedUnlockedAchievements | undefined>(
|
||||
"/profile/games/achievements",
|
||||
{
|
||||
id: game.remoteId,
|
||||
@@ -123,10 +125,19 @@ export const mergeAchievements = async (
|
||||
{ needsSubscription: !newAchievements.length }
|
||||
)
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
return saveAchievementsOnLocal(
|
||||
response.objectId,
|
||||
response.shop,
|
||||
response.achievements,
|
||||
publishNotification
|
||||
);
|
||||
}
|
||||
|
||||
return saveAchievementsOnLocal(
|
||||
response.objectId,
|
||||
response.shop,
|
||||
response.achievements,
|
||||
game.objectId,
|
||||
game.shop,
|
||||
mergedLocalAchievements,
|
||||
publishNotification
|
||||
);
|
||||
})
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import path from "node:path";
|
||||
import cp from "node:child_process";
|
||||
import { app } from "electron";
|
||||
|
||||
export const startAria2 = () => {};
|
||||
|
||||
export class Aria2 {
|
||||
private static process: cp.ChildProcess | null = null;
|
||||
|
||||
public static spawn() {
|
||||
const binaryPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "aria2", "aria2c")
|
||||
: path.join(__dirname, "..", "..", "aria2", "aria2c");
|
||||
|
||||
this.process = cp.spawn(
|
||||
binaryPath,
|
||||
[
|
||||
"--enable-rpc",
|
||||
"--rpc-listen-all",
|
||||
"--file-allocation=none",
|
||||
"--allow-overwrite=true",
|
||||
],
|
||||
{ stdio: "inherit", windowsHide: true }
|
||||
);
|
||||
}
|
||||
|
||||
public static kill() {
|
||||
this.process?.kill();
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import { logger } from "./logger";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import axios from "axios";
|
||||
import { Ludusavi } from "./ludusavi";
|
||||
import { isFuture, isToday } from "date-fns";
|
||||
import { SubscriptionRequiredError } from "@shared";
|
||||
|
||||
export class CloudSync {
|
||||
@@ -54,8 +53,8 @@ export class CloudSync {
|
||||
const hasActiveSubscription = await db
|
||||
.get<string, User>(levelKeys.user, { valueEncoding: "json" })
|
||||
.then((user) => {
|
||||
const expiresAt = user?.subscription?.expiresAt;
|
||||
return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
|
||||
const expiresAt = new Date(user?.subscription?.expiresAt ?? 0);
|
||||
return expiresAt > new Date();
|
||||
});
|
||||
|
||||
if (!hasActiveSubscription) {
|
||||
|
||||
109
src/main/services/common-redist-manager.ts
Normal file
109
src/main/services/common-redist-manager.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { commonRedistPath } from "@main/constants";
|
||||
import axios from "axios";
|
||||
import fs from "node:fs";
|
||||
import cp from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { logger } from "./logger";
|
||||
import { app } from "electron";
|
||||
import { WindowManager } from "./window-manager";
|
||||
|
||||
export class CommonRedistManager {
|
||||
private static readonly redistributables = [
|
||||
"dotNetFx40_Full_setup.exe",
|
||||
"dxwebsetup.exe",
|
||||
"oalinst.exe",
|
||||
"install.bat",
|
||||
"vcredist_2015-2019_x64.exe",
|
||||
"vcredist_2015-2019_x86.exe",
|
||||
"vcredist_x64.exe",
|
||||
"vcredist_x86.exe",
|
||||
"xnafx40_redist.msi",
|
||||
];
|
||||
private static readonly installationTimeout = 1000 * 60 * 5; // 5 minutes
|
||||
private static readonly installationLog = path.join(
|
||||
app.getPath("temp"),
|
||||
"common_redist_install.log"
|
||||
);
|
||||
|
||||
public static async installCommonRedist() {
|
||||
const abortController = new AbortController();
|
||||
const timeout = setTimeout(() => {
|
||||
abortController.abort();
|
||||
logger.error("Installation timed out");
|
||||
|
||||
WindowManager.mainWindow?.webContents.send("common-redist-progress", {
|
||||
log: "Installation timed out",
|
||||
complete: false,
|
||||
});
|
||||
}, this.installationTimeout);
|
||||
|
||||
const installationCompleteMessage = "Installation complete";
|
||||
|
||||
if (!fs.existsSync(this.installationLog)) {
|
||||
await fs.promises.writeFile(this.installationLog, "");
|
||||
}
|
||||
|
||||
fs.watch(this.installationLog, { signal: abortController.signal }, () => {
|
||||
fs.readFile(this.installationLog, "utf-8", (err, data) => {
|
||||
if (err) return logger.error("Error reading log file:", err);
|
||||
|
||||
const tail = data.split("\n").at(-2)?.trim();
|
||||
|
||||
if (tail?.includes(installationCompleteMessage)) {
|
||||
clearTimeout(timeout);
|
||||
if (!abortController.signal.aborted) {
|
||||
abortController.abort();
|
||||
}
|
||||
}
|
||||
|
||||
WindowManager.mainWindow?.webContents.send("common-redist-progress", {
|
||||
log: tail,
|
||||
complete: tail?.includes(installationCompleteMessage),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cp.exec(
|
||||
path.join(commonRedistPath, "install.bat"),
|
||||
{
|
||||
windowsHide: true,
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
logger.error("Failed to run install.bat", error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static async canInstallCommonRedist() {
|
||||
return this.redistributables.every((redist) => {
|
||||
const filePath = path.join(commonRedistPath, redist);
|
||||
|
||||
return fs.existsSync(filePath);
|
||||
});
|
||||
}
|
||||
|
||||
public static async downloadCommonRedist() {
|
||||
if (!fs.existsSync(commonRedistPath)) {
|
||||
await fs.promises.mkdir(commonRedistPath, { recursive: true });
|
||||
}
|
||||
|
||||
for (const redist of this.redistributables) {
|
||||
const filePath = path.join(commonRedistPath, redist);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const response = await axios.get(
|
||||
`https://github.com/hydralauncher/hydra-common-redist/raw/refs/heads/main/${redist}`,
|
||||
{
|
||||
responseType: "arraybuffer",
|
||||
}
|
||||
);
|
||||
|
||||
await fs.promises.writeFile(filePath, response.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
import { Downloader, DownloadError } from "@shared";
|
||||
import { Downloader, DownloadError, FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { publishDownloadCompleteNotification } from "../notifications";
|
||||
import type { Download, DownloadProgress, UserPreferences } from "@types";
|
||||
import { GofileApi, QiwiApi, DatanodesApi, MediafireApi } from "../hosters";
|
||||
import {
|
||||
GofileApi,
|
||||
QiwiApi,
|
||||
DatanodesApi,
|
||||
MediafireApi,
|
||||
PixelDrainApi,
|
||||
} from "../hosters";
|
||||
import { PythonRPC } from "../python-rpc";
|
||||
import {
|
||||
LibtorrentPayload,
|
||||
@@ -16,6 +22,8 @@ import { logger } from "../logger";
|
||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { sortBy } from "lodash-es";
|
||||
import { TorBoxClient } from "./torbox";
|
||||
import { GameFilesManager } from "../game-files-manager";
|
||||
import { HydraDebridClient } from "./hydra-debrid";
|
||||
|
||||
export class DownloadManager {
|
||||
private static downloadingGameId: string | null = null;
|
||||
@@ -130,6 +138,8 @@ export class DownloadManager {
|
||||
);
|
||||
}
|
||||
|
||||
const shouldExtract = download.automaticallyExtract;
|
||||
|
||||
if (progress === 1 && download) {
|
||||
publishDownloadCompleteNotification(game);
|
||||
|
||||
@@ -137,23 +147,48 @@ export class DownloadManager {
|
||||
userPreferences?.seedAfterDownloadComplete &&
|
||||
download.downloader === Downloader.Torrent
|
||||
) {
|
||||
downloadsSublevel.put(gameId, {
|
||||
await downloadsSublevel.put(gameId, {
|
||||
...download,
|
||||
status: "seeding",
|
||||
shouldSeed: true,
|
||||
queued: false,
|
||||
extracting: shouldExtract,
|
||||
});
|
||||
} else {
|
||||
downloadsSublevel.put(gameId, {
|
||||
await downloadsSublevel.put(gameId, {
|
||||
...download,
|
||||
status: "complete",
|
||||
shouldSeed: false,
|
||||
queued: false,
|
||||
extracting: shouldExtract,
|
||||
});
|
||||
|
||||
this.cancelDownload(gameId);
|
||||
}
|
||||
|
||||
if (shouldExtract) {
|
||||
const gameFilesManager = new GameFilesManager(
|
||||
game.shop,
|
||||
game.objectId
|
||||
);
|
||||
|
||||
if (
|
||||
FILE_EXTENSIONS_TO_EXTRACT.some((ext) =>
|
||||
download.folderName?.endsWith(ext)
|
||||
)
|
||||
) {
|
||||
gameFilesManager.extractDownloadedFile();
|
||||
} else {
|
||||
gameFilesManager
|
||||
.extractFilesInDirectory(
|
||||
path.join(download.downloadPath, download.folderName!)
|
||||
)
|
||||
.then(() => {
|
||||
gameFilesManager.setExtractionComplete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const downloads = await downloadsSublevel
|
||||
.values()
|
||||
.all()
|
||||
@@ -279,15 +314,18 @@ export class DownloadManager {
|
||||
url: downloadLink,
|
||||
save_path: download.downloadPath,
|
||||
header: `Cookie: accountToken=${token}`,
|
||||
allow_multiple_connections: true,
|
||||
connections_limit: 8,
|
||||
};
|
||||
}
|
||||
case Downloader.PixelDrain: {
|
||||
const id = download.uri.split("/").pop();
|
||||
const downloadUrl = await PixelDrainApi.getDownloadUrl(id!);
|
||||
|
||||
return {
|
||||
action: "start",
|
||||
game_id: downloadId,
|
||||
url: `https://cdn.pd5-gamedriveorg.workers.dev/api/file/${id}`,
|
||||
url: downloadUrl,
|
||||
save_path: download.downloadPath,
|
||||
};
|
||||
}
|
||||
@@ -336,6 +374,7 @@ export class DownloadManager {
|
||||
game_id: downloadId,
|
||||
url: downloadUrl,
|
||||
save_path: download.downloadPath,
|
||||
allow_multiple_connections: true,
|
||||
};
|
||||
}
|
||||
case Downloader.TorBox: {
|
||||
@@ -348,6 +387,22 @@ export class DownloadManager {
|
||||
url,
|
||||
save_path: download.downloadPath,
|
||||
out: name,
|
||||
allow_multiple_connections: true,
|
||||
};
|
||||
}
|
||||
case Downloader.Hydra: {
|
||||
const downloadUrl = await HydraDebridClient.getDownloadUrl(
|
||||
download.uri
|
||||
);
|
||||
|
||||
if (!downloadUrl) throw new Error(DownloadError.NotCachedInHydra);
|
||||
|
||||
return {
|
||||
action: "start",
|
||||
game_id: downloadId,
|
||||
url: downloadUrl,
|
||||
save_path: download.downloadPath,
|
||||
allow_multiple_connections: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
27
src/main/services/download/hydra-debrid.ts
Normal file
27
src/main/services/download/hydra-debrid.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { HydraApi } from "../hydra-api";
|
||||
|
||||
export class HydraDebridClient {
|
||||
public static getAvailableMagnets(
|
||||
magnets: string[]
|
||||
): Promise<Record<string, boolean>> {
|
||||
return HydraApi.put(
|
||||
"/debrid/check-availability",
|
||||
{
|
||||
magnets,
|
||||
},
|
||||
{ needsAuth: false }
|
||||
);
|
||||
}
|
||||
|
||||
public static async getDownloadUrl(magnet: string) {
|
||||
try {
|
||||
const response = await HydraApi.post("/debrid/request-file", {
|
||||
magnet,
|
||||
});
|
||||
|
||||
return response.downloadUrl;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/main/services/game-files-manager.ts
Normal file
158
src/main/services/game-files-manager.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import type { GameShop } from "@types";
|
||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
|
||||
import { SevenZip } from "./7zip";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { publishExtractionCompleteNotification } from "./notifications";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export class GameFilesManager {
|
||||
constructor(
|
||||
private readonly shop: GameShop,
|
||||
private readonly objectId: string
|
||||
) {}
|
||||
|
||||
private async clearExtractionState() {
|
||||
const gameKey = levelKeys.game(this.shop, this.objectId);
|
||||
const download = await downloadsSublevel.get(gameKey);
|
||||
|
||||
await downloadsSublevel.put(gameKey, {
|
||||
...download!,
|
||||
extracting: false,
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
"on-extraction-complete",
|
||||
this.shop,
|
||||
this.objectId
|
||||
);
|
||||
}
|
||||
|
||||
async extractFilesInDirectory(directoryPath: string) {
|
||||
if (!fs.existsSync(directoryPath)) return;
|
||||
const files = await fs.promises.readdir(directoryPath);
|
||||
|
||||
const compressedFiles = files.filter((file) =>
|
||||
FILE_EXTENSIONS_TO_EXTRACT.some((ext) => file.endsWith(ext))
|
||||
);
|
||||
|
||||
const filesToExtract = compressedFiles.filter(
|
||||
(file) => /part1\.rar$/i.test(file) || !/part\d+\.rar$/i.test(file)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
filesToExtract.map((file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
SevenZip.extractFile(
|
||||
{
|
||||
filePath: path.join(directoryPath, file),
|
||||
cwd: directoryPath,
|
||||
passwords: ["online-fix.me", "steamrip.com"],
|
||||
},
|
||||
() => {
|
||||
resolve(true);
|
||||
},
|
||||
() => {
|
||||
reject(new Error(`Failed to extract file: ${file}`));
|
||||
this.clearExtractionState();
|
||||
}
|
||||
);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
compressedFiles.forEach((file) => {
|
||||
const extractionPath = path.join(directoryPath, file);
|
||||
|
||||
if (fs.existsSync(extractionPath)) {
|
||||
fs.unlink(extractionPath, (err) => {
|
||||
if (err) {
|
||||
logger.error(`Failed to delete file: ${file}`, err);
|
||||
|
||||
this.clearExtractionState();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setExtractionComplete(publishNotification = true) {
|
||||
const gameKey = levelKeys.game(this.shop, this.objectId);
|
||||
|
||||
const [download, game] = await Promise.all([
|
||||
downloadsSublevel.get(gameKey),
|
||||
gamesSublevel.get(gameKey),
|
||||
]);
|
||||
|
||||
await downloadsSublevel.put(gameKey, {
|
||||
...download!,
|
||||
extracting: false,
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
"on-extraction-complete",
|
||||
this.shop,
|
||||
this.objectId
|
||||
);
|
||||
|
||||
if (publishNotification) {
|
||||
publishExtractionCompleteNotification(game!);
|
||||
}
|
||||
}
|
||||
|
||||
async extractDownloadedFile() {
|
||||
const gameKey = levelKeys.game(this.shop, this.objectId);
|
||||
|
||||
const [download, game] = await Promise.all([
|
||||
downloadsSublevel.get(gameKey),
|
||||
gamesSublevel.get(gameKey),
|
||||
]);
|
||||
|
||||
if (!download || !game) return false;
|
||||
|
||||
const filePath = path.join(download.downloadPath, download.folderName!);
|
||||
|
||||
const extractionPath = path.join(
|
||||
download.downloadPath,
|
||||
path.parse(download.folderName!).name
|
||||
);
|
||||
|
||||
SevenZip.extractFile(
|
||||
{
|
||||
filePath,
|
||||
outputPath: extractionPath,
|
||||
passwords: ["online-fix.me", "steamrip.com"],
|
||||
},
|
||||
async () => {
|
||||
await this.extractFilesInDirectory(extractionPath);
|
||||
|
||||
if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) {
|
||||
fs.unlink(filePath, (err) => {
|
||||
if (err) {
|
||||
logger.error(
|
||||
`Failed to delete file: ${download.folderName}`,
|
||||
err
|
||||
);
|
||||
|
||||
this.clearExtractionState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await downloadsSublevel.put(gameKey, {
|
||||
...download!,
|
||||
folderName: path.parse(download.folderName!).name,
|
||||
});
|
||||
|
||||
this.setExtractionComplete();
|
||||
},
|
||||
() => {
|
||||
this.clearExtractionState();
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,71 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { wrapper } from "axios-cookiejar-support";
|
||||
import { CookieJar } from "tough-cookie";
|
||||
|
||||
export class DatanodesApi {
|
||||
private static readonly session = axios.create({});
|
||||
private static readonly jar = new CookieJar();
|
||||
|
||||
private static readonly session = wrapper(
|
||||
axios.create({
|
||||
jar: DatanodesApi.jar,
|
||||
withCredentials: true,
|
||||
})
|
||||
);
|
||||
|
||||
public static async getDownloadUrl(downloadUrl: string): Promise<string> {
|
||||
const parsedUrl = new URL(downloadUrl);
|
||||
const pathSegments = parsedUrl.pathname.split("/");
|
||||
try {
|
||||
const parsedUrl = new URL(downloadUrl);
|
||||
const pathSegments = parsedUrl.pathname.split("/").filter(Boolean);
|
||||
const fileCode = pathSegments[0];
|
||||
|
||||
const fileCode = decodeURIComponent(pathSegments[1]);
|
||||
const fileName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
|
||||
await this.jar.setCookie("lang=english;", "https://datanodes.to");
|
||||
|
||||
const payload = new URLSearchParams({
|
||||
op: "download2",
|
||||
id: fileCode,
|
||||
rand: "",
|
||||
referer: "https://datanodes.to/download",
|
||||
method_free: "Free Download >>",
|
||||
method_premium: "",
|
||||
adblock_detected: "",
|
||||
});
|
||||
const payload = new URLSearchParams({
|
||||
op: "download2",
|
||||
id: fileCode,
|
||||
method_free: "Free Download >>",
|
||||
dl: "1",
|
||||
});
|
||||
|
||||
const response: AxiosResponse = await this.session.post(
|
||||
"https://datanodes.to/download",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Cookie: `lang=english; file_name=${fileName}; file_code=${fileCode};`,
|
||||
Host: "datanodes.to",
|
||||
Origin: "https://datanodes.to",
|
||||
Referer: "https://datanodes.to/download",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
||||
},
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status: number) => status === 302 || status < 400,
|
||||
const response: AxiosResponse = await this.session.post(
|
||||
"https://datanodes.to/download",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0",
|
||||
Referer: "https://datanodes.to/download",
|
||||
Origin: "https://datanodes.to",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status: number) => status === 302 || status < 400,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 302) {
|
||||
return response.headers["location"];
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 302) {
|
||||
return response.headers["location"];
|
||||
if (typeof response.data === "object" && response.data.url) {
|
||||
return decodeURIComponent(response.data.url);
|
||||
}
|
||||
|
||||
const htmlContent = String(response.data);
|
||||
if (!htmlContent) {
|
||||
throw new Error("Empty response received");
|
||||
}
|
||||
|
||||
const downloadLinkRegex = /href=["'](https:\/\/[^"']+)["']/;
|
||||
const downloadLinkMatch = downloadLinkRegex.exec(htmlContent);
|
||||
if (downloadLinkMatch) {
|
||||
return downloadLinkMatch[1];
|
||||
}
|
||||
|
||||
throw new Error("Failed to get the download link");
|
||||
} catch (error) {
|
||||
console.error("Error fetching download URL:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./gofile";
|
||||
export * from "./qiwi";
|
||||
export * from "./datanodes";
|
||||
export * from "./mediafire";
|
||||
export * from "./pixeldrain";
|
||||
|
||||
42
src/main/services/hosters/pixeldrain.ts
Normal file
42
src/main/services/hosters/pixeldrain.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import axios from "axios";
|
||||
|
||||
export class PixelDrainApi {
|
||||
private static readonly browserHeaders = {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
Accept:
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
DNT: "1",
|
||||
Connection: "keep-alive",
|
||||
"Sec-Fetch-Dest": "document",
|
||||
"Sec-Fetch-Mode": "navigate",
|
||||
"Sec-Fetch-Site": "none",
|
||||
"Sec-Fetch-User": "?1",
|
||||
};
|
||||
|
||||
public static async getDownloadUrl(fileId: string): Promise<string> {
|
||||
try {
|
||||
const response = await axios.get(`https://pd.cybar.xyz/${fileId}`, {
|
||||
headers: this.browserHeaders,
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) =>
|
||||
status === 301 || status === 302 || status === 200,
|
||||
});
|
||||
|
||||
if (
|
||||
response.headers.location ||
|
||||
response.status === 301 ||
|
||||
response.status === 302
|
||||
) {
|
||||
return response.headers.location;
|
||||
}
|
||||
|
||||
throw new Error(`No redirect URL found (status: ${response.status})`);
|
||||
} catch (error) {
|
||||
console.error("Error fetching PixelDrain URL:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
|
||||
import { omit } from "lodash-es";
|
||||
import { appVersion } from "@main/constants";
|
||||
import { getUserData } from "./user/get-user-data";
|
||||
import { isFuture, isToday } from "date-fns";
|
||||
import { db } from "@main/level";
|
||||
import { levelKeys } from "@main/level/sublevels";
|
||||
import type { Auth, User } from "@types";
|
||||
@@ -47,8 +46,8 @@ export class HydraApi {
|
||||
}
|
||||
|
||||
private static hasActiveSubscription() {
|
||||
const expiresAt = this.userAuth.subscription?.expiresAt;
|
||||
return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
|
||||
const expiresAt = new Date(this.userAuth.subscription?.expiresAt ?? 0);
|
||||
return expiresAt > new Date();
|
||||
}
|
||||
|
||||
static async handleExternalAuth(uri: string) {
|
||||
|
||||
@@ -8,3 +8,6 @@ export * from "./main-loop";
|
||||
export * from "./hydra-api";
|
||||
export * from "./ludusavi";
|
||||
export * from "./cloud-sync";
|
||||
export * from "./7zip";
|
||||
export * from "./game-files-manager";
|
||||
export * from "./common-redist-manager";
|
||||
|
||||
@@ -3,18 +3,21 @@ import { DownloadManager } from "./download";
|
||||
import { watchProcesses } from "./process-watcher";
|
||||
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
|
||||
import { UpdateManager } from "./update-manager";
|
||||
import { watchFriendRequests } from "@main/events/profile/sync-friend-requests";
|
||||
import { MAIN_LOOP_INTERVAL } from "@main/constants";
|
||||
|
||||
export const startMainLoop = async () => {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
await Promise.allSettled([
|
||||
watchProcesses(),
|
||||
watchFriendRequests(),
|
||||
DownloadManager.watchDownloads(),
|
||||
AchievementWatcherManager.watchAchievements(),
|
||||
DownloadManager.getSeedStatus(),
|
||||
UpdateManager.checkForUpdatePeriodically(),
|
||||
]);
|
||||
|
||||
await sleep(1500);
|
||||
await sleep(MAIN_LOOP_INTERVAL);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { logger } from "../logger";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import type { Game, UserPreferences } from "@types";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update";
|
||||
|
||||
async function downloadImage(url: string | null) {
|
||||
if (!url) return undefined;
|
||||
@@ -72,10 +73,33 @@ export const publishNotificationUpdateReadyToInstall = async (
|
||||
ns: "notifications",
|
||||
}),
|
||||
icon: trayIcon,
|
||||
}).show();
|
||||
})
|
||||
.on("click", () => {
|
||||
restartAndInstallUpdate();
|
||||
})
|
||||
.show();
|
||||
};
|
||||
|
||||
export const publishNewFriendRequestNotification = async () => {};
|
||||
export const publishNewFriendRequestNotification = async () => {
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (!userPreferences?.friendRequestNotificationsEnabled) return;
|
||||
|
||||
new Notification({
|
||||
title: t("new_friend_request_title", {
|
||||
ns: "notifications",
|
||||
}),
|
||||
body: t("new_friend_request_description", {
|
||||
ns: "notifications",
|
||||
}),
|
||||
icon: trayIcon,
|
||||
}).show();
|
||||
};
|
||||
|
||||
export const publishCombinedNewAchievementNotification = async (
|
||||
achievementCount,
|
||||
@@ -104,6 +128,17 @@ export const publishCombinedNewAchievementNotification = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const publishExtractionCompleteNotification = async (game: Game) => {
|
||||
new Notification({
|
||||
title: t("extraction_complete", { ns: "notifications" }),
|
||||
body: t("game_extracted", {
|
||||
ns: "notifications",
|
||||
title: game.title,
|
||||
}),
|
||||
icon: trayIcon,
|
||||
}).show();
|
||||
};
|
||||
|
||||
export const publishNewAchievementNotification = async (info: {
|
||||
achievements: { displayName: string; iconUrl: string }[];
|
||||
unlockedAchievementCount: number;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user