mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 22:06:17 +00:00
Compare commits
291 Commits
v3.2.0
...
chore/remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
497f5e7742 | ||
|
|
199b0d5b19 | ||
|
|
2554dc4c69 | ||
|
|
c4c401e054 | ||
|
|
864ff0070f | ||
|
|
da8c40d5dc | ||
|
|
a32fdf3385 | ||
|
|
c9c1750afb | ||
|
|
857063d2c7 | ||
|
|
ed699a8dee | ||
|
|
a9ec1a62bf | ||
|
|
778e921594 | ||
|
|
206886c091 | ||
|
|
af2896efc3 | ||
|
|
c7735362e0 | ||
|
|
886e176b08 | ||
|
|
f522a7c9ef | ||
|
|
69f4ce821f | ||
|
|
8bfd6e5547 | ||
|
|
1cbde684e7 | ||
|
|
e5f7e9addc | ||
|
|
6832c11d06 | ||
|
|
740cc2ec7c | ||
|
|
e12c6daa16 | ||
|
|
2ee8a64945 | ||
|
|
a54edd8b4a | ||
|
|
f7b6a91621 | ||
|
|
96dbfe8593 | ||
|
|
8513f83169 | ||
|
|
b116e29dc0 | ||
|
|
52b291fb24 | ||
|
|
260a11ba6a | ||
|
|
adf3bf38a8 | ||
|
|
b6193636dd | ||
|
|
923601bdef | ||
|
|
58855a93a8 | ||
|
|
d879f2e3df | ||
|
|
73ab3872a7 | ||
|
|
726a39a430 | ||
|
|
741364e922 | ||
|
|
92ac5b0d1a | ||
|
|
4641b1967e | ||
|
|
0bf70ffebd | ||
|
|
21cec50e2e | ||
|
|
0724a40cb7 | ||
|
|
e066ea3503 | ||
|
|
cf202c8f01 | ||
|
|
75a44bed3f | ||
|
|
c5206c68ee | ||
|
|
0b4c3a6cd2 | ||
|
|
86847ec50e | ||
|
|
e086369b13 | ||
|
|
e695d599de | ||
|
|
ad55165078 | ||
|
|
385e29262c | ||
|
|
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=
|
||||
|
||||
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Build
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
@@ -7,6 +11,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -27,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
|
||||
|
||||
@@ -43,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'
|
||||
@@ -57,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 }}
|
||||
@@ -74,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
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Lint
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
|
||||
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Release
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: main
|
||||
@@ -29,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
|
||||
|
||||
@@ -45,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'
|
||||
@@ -60,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: {
|
||||
|
||||
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "3.2.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",
|
||||
@@ -71,9 +69,11 @@
|
||||
"react-loading-skeleton": "^3.4.0",
|
||||
"react-redux": "^9.1.1",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"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",
|
||||
@@ -100,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", "10",
|
||||
"--buffer-size", "16",
|
||||
"--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', 24))
|
||||
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', 24))
|
||||
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', 24))
|
||||
|
||||
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
|
||||
966
rust_rpc/src/main.rs
Normal file
966
rust_rpc/src/main.rs
Normal file
@@ -0,0 +1,966 @@
|
||||
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.inc(downloaded);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
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()?;
|
||||
}
|
||||
|
||||
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 temp_path = format!("{}.tmp", self.file_path);
|
||||
let source = File::open(&self.file_path)?;
|
||||
let dest = File::create(&temp_path)?;
|
||||
|
||||
let mut reader = BufReader::with_capacity(FINALIZE_BUFFER_SIZE, source);
|
||||
let mut writer = BufWriter::with_capacity(FINALIZE_BUFFER_SIZE, dest);
|
||||
|
||||
reader.seek(SeekFrom::Start(HEADER_SIZE as u64))?;
|
||||
|
||||
std::io::copy(&mut reader, &mut writer)?;
|
||||
writer.flush()?;
|
||||
drop(writer);
|
||||
|
||||
match std::fs::rename(&temp_path, &self.file_path) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => {
|
||||
let _ = std::fs::remove_file(&self.file_path);
|
||||
std::fs::rename(&temp_path, &self.file_path)?;
|
||||
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
@@ -7,18 +7,18 @@
|
||||
"featured": "مميز",
|
||||
"surprise_me": "مفاجئني",
|
||||
"no_results": "لم يتم العثور على نتائج",
|
||||
"start_typing": "ابدأ الكتابة للبحث...",
|
||||
"hot": "الأكثر شيوعًا الآن",
|
||||
"start_typing": "ابدأ بالكتابة للبحث...",
|
||||
"hot": "الأكثر شهرة الآن",
|
||||
"weekly": "📅 أفضل ألعاب الأسبوع",
|
||||
"achievements": "🏆 ألعاب للتغلب عليها"
|
||||
"achievements": "🏆 ألعاب يجب إكمالها"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "الكـتالوج",
|
||||
"catalogue": "الفهرس",
|
||||
"downloads": "التنزيلات",
|
||||
"settings": "الإعدادات",
|
||||
"my_library": "مكتبتي",
|
||||
"downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)",
|
||||
"paused": "{{title}} (معلّق)",
|
||||
"downloading_metadata": "{{title}} (جاري تنزيل البيانات الوصفية...)",
|
||||
"paused": "{{title}} (معلق)",
|
||||
"downloading": "{{title}} ({{percentage}} - جاري التنزيل...)",
|
||||
"filter": "تصفية المكتبة",
|
||||
"home": "الرئيسية",
|
||||
@@ -26,12 +26,13 @@
|
||||
"game_has_no_executable": "اللعبة لا تحتوي على ملف تشغيل",
|
||||
"sign_in": "تسجيل الدخول",
|
||||
"friends": "الأصدقاء",
|
||||
"need_help": "تحتاج مساعدة؟"
|
||||
"need_help": "تحتاج مساعدة؟",
|
||||
"favorites": "المفضلة"
|
||||
},
|
||||
"header": {
|
||||
"search": "ابحث عن الألعاب",
|
||||
"search": "بحث الألعاب",
|
||||
"home": "الرئيسية",
|
||||
"catalogue": "الكـتالوج",
|
||||
"catalogue": "الفهرس",
|
||||
"downloads": "التنزيلات",
|
||||
"search_results": "نتائج البحث",
|
||||
"settings": "الإعدادات",
|
||||
@@ -40,16 +41,16 @@
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "لا توجد تنزيلات قيد التقدم",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية لـ {{title}}...",
|
||||
"downloading": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - الوقت المتبقي {{eta}} - السرعة {{speed}}",
|
||||
"calculating_eta": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - جاري حساب الوقت المتبقي...",
|
||||
"checking_files": "جارٍ فحص ملفات {{title}}... ({{percentage}} اكتمال)"
|
||||
"downloading_metadata": "جاري تنزيل بيانات {{title}} الوصفية...",
|
||||
"downloading": "جاري تنزيل {{title}}... ({{percentage}} مكتمل) - الوقت المتبقي {{eta}} - السرعة {{speed}}",
|
||||
"calculating_eta": "جاري تنزيل {{title}}... ({{percentage}} مكتمل) - جاري حساب الوقت المتبقي...",
|
||||
"checking_files": "جاري فحص ملفات {{title}}... ({{percentage}} مكتمل)"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "تصفية...",
|
||||
"developers": "المطورون",
|
||||
"genres": "الأنواع",
|
||||
"tags": "العلامات",
|
||||
"tags": "الوسوم",
|
||||
"publishers": "الناشرون",
|
||||
"download_sources": "مصادر التنزيل",
|
||||
"result_count": "{{resultCount}} نتيجة",
|
||||
@@ -68,34 +69,34 @@
|
||||
"cancel": "إلغاء",
|
||||
"remove": "إزالة",
|
||||
"space_left_on_disk": "{{space}} متبقي على القرص",
|
||||
"eta": "الانتهاء {{eta}}",
|
||||
"calculating_eta": "جارٍ حساب الوقت المتبقي...",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
||||
"filter": "تصفية الحزم المعاد تعبئتها",
|
||||
"eta": "الانتهاء المتوقع {{eta}}",
|
||||
"calculating_eta": "جاري حساب الوقت المتبقي...",
|
||||
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
|
||||
"filter": "تصفية الإصدارات المعادة",
|
||||
"requirements": "متطلبات النظام",
|
||||
"minimum": "الحد الأدنى",
|
||||
"recommended": "مُوصى به",
|
||||
"paused": "معلّق",
|
||||
"recommended": "مستحسن",
|
||||
"paused": "معلق",
|
||||
"release_date": "تاريخ الإصدار {{date}}",
|
||||
"publisher": "نشر بواسطة {{publisher}}",
|
||||
"hours": "ساعات",
|
||||
"minutes": "دقائق",
|
||||
"amount_hours": "{{amount}} ساعات",
|
||||
"amount_minutes": "{{amount}} دقائق",
|
||||
"amount_hours": "{{amount}} ساعة",
|
||||
"amount_minutes": "{{amount}} دقيقة",
|
||||
"accuracy": "دقة {{accuracy}}%",
|
||||
"add_to_library": "إضافة إلى المكتبة",
|
||||
"remove_from_library": "إزالة من المكتبة",
|
||||
"no_downloads": "لا توجد تنزيلات متاحة",
|
||||
"play_time": "لعب لمدة {{amount}}",
|
||||
"last_time_played": "آخر تشغيل {{period}}",
|
||||
"play_time": "وقت اللعب {{amount}}",
|
||||
"last_time_played": "آخر مرة لعب {{period}}",
|
||||
"not_played_yet": "لم تلعب {{title}} بعد",
|
||||
"next_suggestion": "الاقتراح التالي",
|
||||
"play": "تشغيل",
|
||||
"deleting": "جارٍ حذف المثبت...",
|
||||
"deleting": "جاري حذف المثبت...",
|
||||
"close": "إغلاق",
|
||||
"playing_now": "يتم التشغيل الآن",
|
||||
"playing_now": "جاري التشغيل الآن",
|
||||
"change": "تغيير",
|
||||
"repacks_modal_description": "اختر الحزمة المعاد تعبئتها التي تريد تنزيلها",
|
||||
"repacks_modal_description": "اختر الإصدار المعاد الذي تريد تنزيله",
|
||||
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى <0>الإعدادات</0>",
|
||||
"download_now": "تنزيل الآن",
|
||||
"no_shop_details": "تعذر الحصول على تفاصيل المتجر.",
|
||||
@@ -110,12 +111,12 @@
|
||||
"select_executable": "تحديد",
|
||||
"no_executable_selected": "لم يتم تحديد ملف تشغيل",
|
||||
"open_folder": "فتح المجلد",
|
||||
"open_download_location": "عرض الملفات المحملة",
|
||||
"open_download_location": "عرض الملفات المنزلة",
|
||||
"create_shortcut": "إنشاء اختصار على سطح المكتب",
|
||||
"clear": "مسح",
|
||||
"remove_files": "إزالة الملفات",
|
||||
"remove_from_library_title": "هل أنت متأكد؟",
|
||||
"remove_from_library_description": "سيؤدي هذا إلى إزالة {{game}} من مكتبتك",
|
||||
"remove_from_library_description": "سيتم إزالة {{game}} من مكتبتك",
|
||||
"options": "خيارات",
|
||||
"executable_section_title": "ملف التشغيل",
|
||||
"executable_section_description": "مسار الملف الذي سيتم تشغيله عند النقر على \"تشغيل\"",
|
||||
@@ -123,35 +124,35 @@
|
||||
"downloads_section_description": "تحقق من التحديثات أو الإصدارات الأخرى لهذه اللعبة",
|
||||
"danger_zone_section_title": "منطقة الخطر",
|
||||
"danger_zone_section_description": "إزالة هذه اللعبة من مكتبتك أو الملفات التي تم تنزيلها بواسطة Hydra",
|
||||
"download_in_progress": "تنزيل قيد التقدم",
|
||||
"download_in_progress": "جاري التنزيل",
|
||||
"download_paused": "التنزيل معلق",
|
||||
"last_downloaded_option": "خيار التنزيل الأخير",
|
||||
"create_shortcut_success": "تم إنشاء الاختصار بنجاح",
|
||||
"create_shortcut_error": "خطأ في إنشاء الاختصار",
|
||||
"nsfw_content_title": "هذه اللعبة تحتوي على محتوى غير لائق",
|
||||
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يناسب جميع الأعمار. هل تريد المتابعة؟",
|
||||
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يكون مناسبًا لجميع الأعمار. هل تريد المتابعة؟",
|
||||
"allow_nsfw_content": "متابعة",
|
||||
"refuse_nsfw_content": "رجوع",
|
||||
"stats": "الإحصائيات",
|
||||
"download_count": "مرات التنزيل",
|
||||
"download_count": "التنزيلات",
|
||||
"player_count": "اللاعبون النشطون",
|
||||
"download_error": "خيار التنزيل هذا غير متاح",
|
||||
"download": "تنزيل",
|
||||
"executable_path_in_use": "مسار التشغيل مستخدم بالفعل بواسطة \"{{game}}\"",
|
||||
"warning": "تحذير:",
|
||||
"hydra_needs_to_remain_open": "لهذا التنزيل، يجب أن يبقى Hydra مفتوحًا حتى اكتماله. إذا أغلق Hydra قبل الاكتمال، ستفقد تقدمك.",
|
||||
"hydra_needs_to_remain_open": "لهذا التنزيل، يجب أن يظل Hydra مفتوحًا حتى اكتماله. إذا تم إغلاق Hydra قبل الاكتمال، ستفقد تقدمك.",
|
||||
"achievements": "الإنجازات",
|
||||
"achievements_count": "الإنجازات {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "حفظ سحابي",
|
||||
"cloud_save_description": "احفظ تقدمك على السحابة واستمر في اللعب من أي جهاز",
|
||||
"cloud_save_description": "احفظ تقدمك في السحابة واستمر في اللعب من أي جهاز",
|
||||
"backups": "النسخ الاحتياطية",
|
||||
"install_backup": "تثبيت",
|
||||
"delete_backup": "حذف",
|
||||
"create_backup": "نسخة احتياطية جديدة",
|
||||
"last_backup_date": "آخر نسخة احتياطية في {{date}}",
|
||||
"no_backup_preview": "لم يتم العثور على حفظات لهذا العنوان",
|
||||
"restoring_backup": "جارٍ استعادة النسخة الاحتياطية ({{progress}} اكتمال)...",
|
||||
"uploading_backup": "جارٍ رفع النسخة الاحتياطية...",
|
||||
"restoring_backup": "جاري استعادة النسخة الاحتياطية ({{progress}} مكتمل)...",
|
||||
"uploading_backup": "جاري رفع النسخة الاحتياطية...",
|
||||
"no_backups": "لم تقم بإنشاء أي نسخ احتياطية لهذه اللعبة بعد",
|
||||
"backup_uploaded": "تم رفع النسخة الاحتياطية",
|
||||
"backup_deleted": "تم حذف النسخة الاحتياطية",
|
||||
@@ -164,61 +165,67 @@
|
||||
"files_automatically_mapped": "تم تعيين الملفات تلقائيًا",
|
||||
"no_backups_created": "لم يتم إنشاء نسخ احتياطية لهذه اللعبة",
|
||||
"manage_files": "إدارة الملفات",
|
||||
"loading_save_preview": "جارٍ البحث عن حفظات الألعاب...",
|
||||
"loading_save_preview": "جاري البحث عن حفظات اللعبة...",
|
||||
"wine_prefix": "بادئة Wine",
|
||||
"wine_prefix_description": "بادئة Wine المستخدمة لتشغيل هذه اللعبة",
|
||||
"launch_options": "خيارات التشغيل",
|
||||
"launch_options_description": "يمكن للمستخدمين المتقدمين إدخال تعديلات على خيارات التشغيل (ميزة تجريبية)",
|
||||
"launch_options_placeholder": "لم يتم تحديد أي معاملات",
|
||||
"launch_options_placeholder": "لا توجد معلمات محددة",
|
||||
"no_download_option_info": "لا توجد معلومات متاحة",
|
||||
"backup_deletion_failed": "فشل حذف النسخة الاحتياطية",
|
||||
"max_number_of_artifacts_reached": "تم الوصول إلى الحد الأقصى لعدد النسخ الاحتياطية لهذه اللعبة",
|
||||
"achievements_not_sync": "تعرف على كيفية مزامنة إنجازاتك",
|
||||
"backup_deletion_failed": "فشل في حذف النسخة الاحتياطية",
|
||||
"max_number_of_artifacts_reached": "تم الوصول إلى الحد الأقصى من النسخ الاحتياطية لهذه اللعبة",
|
||||
"achievements_not_sync": "شاهد كيفية مزامنة إنجازاتك",
|
||||
"manage_files_description": "إدارة الملفات التي سيتم نسخها احتياطيًا واستعادتها",
|
||||
"select_folder": "حدد المجلد",
|
||||
"backup_from": "نسخة احتياطية من {{date}}",
|
||||
"custom_backup_location_set": "تم تعيين موقع نسخ احتياطي مخصص",
|
||||
"no_directory_selected": "لم يتم تحديد مجلد",
|
||||
"no_write_permission": "لا يمكن التنزيل إلى هذا المجلد. انقر هنا لمعرفة المزيد.",
|
||||
"no_write_permission": "لا يمكن التنزيل إلى هذا المجلد. انقر هنا للمزيد من المعلومات.",
|
||||
"reset_achievements": "إعادة تعيين الإنجازات",
|
||||
"reset_achievements_description": "سيؤدي هذا إلى إعادة تعيين جميع إنجازات {{game}}",
|
||||
"reset_achievements_title": "هل أنت متأكد؟",
|
||||
"reset_achievements_success": "تم إعادة تعيين الإنجازات بنجاح",
|
||||
"reset_achievements_error": "فشل إعادة تعيين الإنجازات"
|
||||
"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 غير متاح حاليًا.",
|
||||
"game_removed_from_favorites": "تمت إزالة اللعبة من المفضلة",
|
||||
"game_added_to_favorites": "تمت إضافة اللعبة إلى المفضلة"
|
||||
},
|
||||
"activation": {
|
||||
"title": "تفعيل Hydra",
|
||||
"installation_id": "معرف التثبيت:",
|
||||
"enter_activation_code": "أدخل رمز التفعيل الخاص بك",
|
||||
"message": "إذا كنت لا تعرف أين تطلب هذا، فلا يجب أن يكون لديك هذا.",
|
||||
"message": "إذا كنت لا تعرف أين تطلب هذا، فأنت لا يجب أن يكون لديك هذا.",
|
||||
"activate": "تفعيل",
|
||||
"loading": "جارٍ التحميل..."
|
||||
"loading": "جاري التحميل..."
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "استئناف",
|
||||
"pause": "إيقاف مؤقت",
|
||||
"eta": "الانتهاء {{eta}}",
|
||||
"paused": "معلّق",
|
||||
"verifying": "جارٍ التحقق...",
|
||||
"eta": "الانتهاء المتوقع {{eta}}",
|
||||
"paused": "معلق",
|
||||
"verifying": "جاري التحقق...",
|
||||
"completed": "مكتمل",
|
||||
"removed": "غير محمل",
|
||||
"removed": "غير منزّل",
|
||||
"cancel": "إلغاء",
|
||||
"filter": "تصفية الألعاب المحملة",
|
||||
"filter": "تصفية الألعاب المنزلة",
|
||||
"remove": "إزالة",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
||||
"deleting": "جارٍ حذف المثبت...",
|
||||
"delete": "إزالة المثبت",
|
||||
"downloading_metadata": "جاري تنزيل البيانات الوصفية...",
|
||||
"deleting": "جاري حذف المثبت...",
|
||||
"delete": "حذف المثبت",
|
||||
"delete_modal_title": "هل أنت متأكد؟",
|
||||
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهازك",
|
||||
"install": "تثبيت",
|
||||
"download_in_progress": "قيد التقدم",
|
||||
"queued_downloads": "التنزيلات في قائمة الانتظار",
|
||||
"downloads_completed": "مكتمل",
|
||||
"downloads_completed": "مكتملة",
|
||||
"queued": "في قائمة الانتظار",
|
||||
"no_downloads_title": "فارغ جدًا",
|
||||
"no_downloads_title": "لا شيء هنا",
|
||||
"no_downloads_description": "لم تقم بتنزيل أي شيء باستخدام Hydra بعد، ولكن لم يفت الأوان للبدء.",
|
||||
"checking_files": "جارٍ فحص الملفات...",
|
||||
"seeding": "التوزيع",
|
||||
"checking_files": "جاري فحص الملفات...",
|
||||
"seeding": "جاري التوزيع",
|
||||
"stop_seeding": "إيقاف التوزيع",
|
||||
"resume_seeding": "استئناف التوزيع",
|
||||
"options": "إدارة"
|
||||
@@ -228,8 +235,8 @@
|
||||
"change": "تحديث",
|
||||
"notifications": "الإشعارات",
|
||||
"enable_download_notifications": "عند اكتمال التنزيل",
|
||||
"enable_repack_list_notifications": "عند إضافة حزمة معاد تعبئتها جديدة",
|
||||
"real_debrid_api_token_label": "رمز واجهة برمجة تطبيقات Real-Debrid",
|
||||
"enable_repack_list_notifications": "عند إضافة إصدار معاد جديد",
|
||||
"real_debrid_api_token_label": "رمز Real-Debrid API",
|
||||
"quit_app_instead_hiding": "لا تخفي Hydra عند الإغلاق",
|
||||
"launch_with_system": "تشغيل Hydra مع بدء النظام",
|
||||
"general": "عام",
|
||||
@@ -238,21 +245,27 @@
|
||||
"language": "اللغة",
|
||||
"api_token": "رمز API",
|
||||
"enable_real_debrid": "تفعيل Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، مقيدة فقط بسرعة الإنترنت لديك.",
|
||||
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، محدودة فقط بسرعة اتصالك بالإنترنت.",
|
||||
"debrid_invalid_token": "رمز API غير صالح",
|
||||
"debrid_api_token_hint": "يمكنك الحصول على رمز API الخاص بك <0>هنا</0>",
|
||||
"real_debrid_free_account_error": "الحساب \"{{username}}\" هو حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
||||
"real_debrid_free_account_error": "الحساب \"{{username}}\" حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
||||
"debrid_linked_message": "تم ربط الحساب \"{{username}}\"",
|
||||
"save_changes": "حفظ التغييرات",
|
||||
"changes_saved": "تم حفظ التغييرات بنجاح",
|
||||
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL المصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
||||
"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}} خيار تنزيل",
|
||||
"download_count_other": "{{countFormatted}} خيارات تنزيل",
|
||||
"download_source_url": "عنوان URL لمصدر التنزيل",
|
||||
"download_source_url": "عنوان مصدر التنزيل",
|
||||
"add_download_source_description": "أدخل عنوان URL لملف .json",
|
||||
"download_source_up_to_date": "محدث",
|
||||
"download_source_errored": "خطأ",
|
||||
@@ -272,13 +285,13 @@
|
||||
"profile_visibility": "رؤية الملف الشخصي",
|
||||
"profile_visibility_description": "اختر من يمكنه رؤية ملفك الشخصي ومكتبتك",
|
||||
"required_field": "هذا الحقل مطلوب",
|
||||
"source_already_exists": "تمت إضافة هذا المصدر مسبقًا",
|
||||
"must_be_valid_url": "يجب أن يكون المصدر عنوان URL صالحًا",
|
||||
"source_already_exists": "هذا المصدر مضاف مسبقًا",
|
||||
"must_be_valid_url": "يجب أن يكون المصدر عنوان URL صالح",
|
||||
"blocked_users": "المستخدمون المحظورون",
|
||||
"user_unblocked": "تم إلغاء حظر المستخدم",
|
||||
"enable_achievement_notifications": "عند فتح إنجاز",
|
||||
"launch_minimized": "تشغيل Hydra مصغرًا",
|
||||
"disable_nsfw_alert": "تعطيل تنبيه المحتوى غير اللائق",
|
||||
"disable_nsfw_alert": "تعطيل تنبيهات المحتوى غير اللائق",
|
||||
"seed_after_download_complete": "التوزيع بعد اكتمال التنزيل",
|
||||
"show_hidden_achievement_description": "عرض وصف الإنجازات المخفية قبل فتحها",
|
||||
"account": "الحساب",
|
||||
@@ -296,18 +309,47 @@
|
||||
"become_subscriber": "كن مشتركًا في Hydra Cloud",
|
||||
"subscription_renew_cancelled": "تم تعطيل التجديد التلقائي",
|
||||
"subscription_renews_on": "سيتم تجديد اشتراكك في {{date}}",
|
||||
"bill_sent_until": "سيتم إرسال فاتورتك التالية حتى هذا اليوم"
|
||||
"bill_sent_until": "سيتم إرسال فاتورتك القادمة حتى هذا اليوم",
|
||||
"no_themes": "يبدو أنه ليس لديك أي سمات بعد، لكن لا تقلق، انقر هنا لإنشاء أول تحفة فنية لك.",
|
||||
"editor_tab_code": "الكود",
|
||||
"editor_tab_info": "معلومات",
|
||||
"editor_tab_save": "حفظ",
|
||||
"web_store": "المتجر الإلكتروني",
|
||||
"clear_themes": "مسح",
|
||||
"create_theme": "إنشاء",
|
||||
"create_theme_modal_title": "إنشاء سمة مخصصة",
|
||||
"create_theme_modal_description": "إنشاء سمة جديدة لتخصيص مظهر Hydra",
|
||||
"theme_name": "الاسم",
|
||||
"insert_theme_name": "أدخل اسم السمة",
|
||||
"set_theme": "تعيين السمة",
|
||||
"unset_theme": "إلغاء تعيين السمة",
|
||||
"delete_theme": "حذف السمة",
|
||||
"edit_theme": "تعديل السمة",
|
||||
"delete_all_themes": "حذف جميع السمات",
|
||||
"delete_all_themes_description": "سيؤدي هذا إلى حذف جميع السمات المخصصة الخاصة بك",
|
||||
"delete_theme_description": "سيؤدي هذا إلى حذف السمة {{theme}}",
|
||||
"cancel": "إلغاء",
|
||||
"appearance": "المظهر",
|
||||
"enable_torbox": "تفعيل Torbox",
|
||||
"torbox_description": "TorBox هي خدمة seedbox متميزة تنافس أفضل الخوادم في السوق.",
|
||||
"torbox_account_linked": "تم ربط حساب TorBox",
|
||||
"real_debrid_account_linked": "تم ربط حساب Real-Debrid",
|
||||
"name_min_length": "يجب أن يكون اسم السمة على الأقل 3 أحرف",
|
||||
"import_theme": "استيراد سمة",
|
||||
"import_theme_description": "ستقوم باستيراد {{theme}} من متجر السمات",
|
||||
"error_importing_theme": "خطأ في استيراد السمة",
|
||||
"theme_imported": "تم استيراد السمة بنجاح"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "اكتمل التنزيل",
|
||||
"game_ready_to_install": "{{title}} جاهز للتثبيت",
|
||||
"repack_list_updated": "تم تحديث قائمة الحزم المعاد تعبئتها",
|
||||
"repack_count_one": "تمت إضافة {{count}} حزمة معاد تعبئتها",
|
||||
"repack_count_other": "تمت إضافة {{count}} حزم معاد تعبئتها",
|
||||
"repack_list_updated": "تم تحديث قائمة الإصدارات المعادة",
|
||||
"repack_count_one": "تمت إضافة {{count}} إصدار معاد",
|
||||
"repack_count_other": "تمت إضافة {{count}} إصدارات معادة",
|
||||
"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}} أخرى تم فتحها"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "فتح Hydra",
|
||||
@@ -319,7 +361,7 @@
|
||||
"binary_not_found_modal": {
|
||||
"title": "البرامج غير مثبتة",
|
||||
"description": "لم يتم العثور على ملفات تشغيل Wine أو Lutris على نظامك",
|
||||
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة لينكس الخاصة بك حتى تعمل اللعبة بشكل طبيعي"
|
||||
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة Linux الخاصة بك حتى تعمل اللعبة بشكل طبيعي"
|
||||
},
|
||||
"modal": {
|
||||
"close": "زر الإغلاق"
|
||||
@@ -328,16 +370,16 @@
|
||||
"toggle_password_visibility": "تبديل رؤية كلمة المرور"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} ساعات",
|
||||
"amount_minutes": "{{amount}} دقائق",
|
||||
"last_time_played": "آخر تشغيل {{period}}",
|
||||
"amount_hours": "{{amount}} ساعة",
|
||||
"amount_minutes": "{{amount}} دقيقة",
|
||||
"last_time_played": "آخر مرة لعب {{period}}",
|
||||
"activity": "النشاط الأخير",
|
||||
"library": "المكتبة",
|
||||
"total_play_time": "إجمالي وقت اللعب",
|
||||
"no_recent_activity_title": "همم... لا شيء هنا",
|
||||
"no_recent_activity_title": "لا شيء هنا...",
|
||||
"no_recent_activity_description": "لم تلعب أي ألعاب مؤخرًا. حان الوقت لتغيير ذلك!",
|
||||
"display_name": "اسم العرض",
|
||||
"saving": "جارٍ الحفظ",
|
||||
"saving": "جاري الحفظ",
|
||||
"save": "حفظ",
|
||||
"edit_profile": "تعديل الملف الشخصي",
|
||||
"saved_successfully": "تم الحفظ بنجاح",
|
||||
@@ -346,13 +388,13 @@
|
||||
"cancel": "إلغاء",
|
||||
"successfully_signed_out": "تم تسجيل الخروج بنجاح",
|
||||
"sign_out": "تسجيل الخروج",
|
||||
"playing_for": "يلعب لمدة {{amount}}",
|
||||
"sign_out_modal_text": "مكتبتك مرتبطة بحسابك الحالي. عند تسجيل الخروج، لن تكون مكتبتك مرئية بعد الآن، ولن يتم حفظ أي تقدم. هل تتابع تسجيل الخروج؟",
|
||||
"playing_for": "جاري اللعب لمدة {{amount}}",
|
||||
"sign_out_modal_text": "مكتبتك مرتبطة بحسابك الحالي. عند تسجيل الخروج، لن تكون مكتبتك مرئية، ولن يتم حفظ أي تقدم. هل تتابع تسجيل الخروج؟",
|
||||
"add_friends": "إضافة أصدقاء",
|
||||
"add": "إضافة",
|
||||
"friend_code": "رمز الصديق",
|
||||
"see_profile": "عرض الملف الشخصي",
|
||||
"sending": "جارٍ الإرسال",
|
||||
"sending": "جاري الإرسال",
|
||||
"friend_request_sent": "تم إرسال طلب الصداقة",
|
||||
"friends": "الأصدقاء",
|
||||
"friends_list": "قائمة الأصدقاء",
|
||||
@@ -371,19 +413,19 @@
|
||||
"blocked_users": "المستخدمون المحظورون",
|
||||
"unblock": "إلغاء الحظر",
|
||||
"no_friends_added": "ليس لديك أصدقاء مضافون",
|
||||
"pending": "قيد الانتظار",
|
||||
"pending": "معلق",
|
||||
"no_pending_invites": "ليس لديك دعوات معلقة",
|
||||
"no_blocked_users": "ليس لديك مستخدمون محظورون",
|
||||
"friend_code_copied": "تم نسخ رمز الصديق",
|
||||
"undo_friendship_modal_text": "سيؤدي هذا إلى إلغاء صداقتك مع {{displayName}}",
|
||||
"privacy_hint": "لضبط من يمكنه رؤية هذا، انتقل إلى <0>الإعدادات</0>",
|
||||
"locked_profile": "هذا الملف الشخصي خاص",
|
||||
"image_process_failure": "فشل معالجة الصورة",
|
||||
"image_process_failure": "فشل في معالجة الصورة",
|
||||
"required_field": "هذا الحقل مطلوب",
|
||||
"displayname_min_length": "يجب أن يكون اسم العرض على الأقل 3 أحرف",
|
||||
"displayname_max_length": "يجب ألا يتجاوز اسم العرض 50 حرفًا",
|
||||
"report_profile": "الإبلاغ عن هذا الملف الشخصي",
|
||||
"report_reason": "لماذا تقوم بالإبلاغ عن هذا الملف الشخصي؟",
|
||||
"displayname_max_length": "يجب أن لا يتجاوز اسم العرض 50 حرفًا",
|
||||
"report_profile": "الإبلاغ عن هذا الملف",
|
||||
"report_reason": "لماذا تقوم بالإبلاغ عن هذا الملف؟",
|
||||
"report_description": "معلومات إضافية",
|
||||
"report_description_placeholder": "معلومات إضافية",
|
||||
"report": "الإبلاغ",
|
||||
@@ -393,32 +435,32 @@
|
||||
"report_reason_spam": "بريد عشوائي",
|
||||
"report_reason_other": "أخرى",
|
||||
"profile_reported": "تم الإبلاغ عن الملف الشخصي",
|
||||
"your_friend_code": "رمز صديقك:",
|
||||
"upload_banner": "تحميل بانر",
|
||||
"uploading_banner": "جارٍ تحميل البانر...",
|
||||
"your_friend_code": "رمز الصديق الخاص بك:",
|
||||
"upload_banner": "رفع بانر",
|
||||
"uploading_banner": "جاري رفع البانر...",
|
||||
"background_image_updated": "تم تحديث صورة الخلفية",
|
||||
"stats": "الإحصائيات",
|
||||
"achievements": "إنجازات",
|
||||
"achievements": "الإنجازات",
|
||||
"games": "الألعاب",
|
||||
"top_percentile": "ال{{percentile}}% الأعلى",
|
||||
"top_percentile": "الأعلى {{percentile}}%",
|
||||
"ranking_updated_weekly": "يتم تحديث التصنيف أسبوعيًا",
|
||||
"playing": "يلعب {{game}}",
|
||||
"playing": "جاري لعب {{game}}",
|
||||
"achievements_unlocked": "الإنجازات المفتوحة",
|
||||
"earned_points": "النقاط المكتسبة",
|
||||
"show_achievements_on_profile": "عرض إنجازاتك على ملفك الشخصي",
|
||||
"show_points_on_profile": "عرض نقاطك المكتسبة على ملفك الشخصي"
|
||||
"show_achievements_on_profile": "عرض إنجازاتك في ملفك الشخصي",
|
||||
"show_points_on_profile": "عرض نقاطك المكتسبة في ملفك الشخصي"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "تم فتح الإنجاز",
|
||||
"user_achievements": "إنجازات {{displayName}}",
|
||||
"your_achievements": "إنجازاتك",
|
||||
"unlocked_at": "تم الفتح في: {{date}}",
|
||||
"subscription_needed": "يحتاج إلى اشتراك Hydra Cloud لرؤية هذا المحتوى",
|
||||
"subscription_needed": "يحتاج إلى اشتراك Hydra Cloud لعرض هذا المحتوى",
|
||||
"new_achievements_unlocked": "تم فتح {{achievementCount}} إنجازات جديدة من {{gameCount}} ألعاب",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} إنجازات",
|
||||
"achievements_unlocked_for_game": "تم فتح {{achievementCount}} إنجازات جديدة لـ {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "هذا إنجاز مخفي",
|
||||
"achievement_earn_points": "اكسب {{points}} نقطة مع هذا الإنجاز",
|
||||
"achievement_earn_points": "احصل على {{points}} نقاط مع هذا الإنجاز",
|
||||
"earned_points": "النقاط المكتسبة:",
|
||||
"available_points": "النقاط المتاحة:",
|
||||
"how_to_earn_achievements_points": "كيفية كسب نقاط الإنجازات؟"
|
||||
@@ -428,10 +470,10 @@
|
||||
"subscribe_now": "اشترك الآن",
|
||||
"cloud_saving": "حفظ سحابي",
|
||||
"cloud_achievements": "احفظ إنجازاتك على السحابة",
|
||||
"animated_profile_picture": "صورة ملف شخصي متحركة",
|
||||
"animated_profile_picture": "صورة ملف متحركة",
|
||||
"premium_support": "دعم ممتاز",
|
||||
"show_and_compare_achievements": "اعرض وقارن إنجازاتك مع المستخدمين الآخرين",
|
||||
"animated_profile_banner": "بانر ملف شخصي متحرك",
|
||||
"animated_profile_banner": "بانر ملف متحرك",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "لقد اكتشفت ميزة Hydra Cloud!",
|
||||
"learn_more": "معرفة المزيد"
|
||||
|
||||
@@ -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…",
|
||||
@@ -178,6 +181,8 @@
|
||||
"manage_files_description": "Manage which files will be backed up and restored",
|
||||
"select_folder": "Select folder",
|
||||
"backup_from": "Backup from {{date}}",
|
||||
"automatic_backup_from": "Automatic backup from {{date}}",
|
||||
"enable_automatic_cloud_sync": "Enable automatic cloud sync",
|
||||
"custom_backup_location_set": "Custom backup location set",
|
||||
"no_directory_selected": "No directory selected",
|
||||
"no_write_permission": "Cannot download into this directory. Click here to learn more.",
|
||||
@@ -191,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",
|
||||
@@ -228,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",
|
||||
@@ -265,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",
|
||||
@@ -327,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",
|
||||
@@ -343,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": {
|
||||
@@ -470,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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,8 @@
|
||||
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
|
||||
"select_folder": "Seleccionar carpeta",
|
||||
"backup_from": "Copia de seguridad de {{date}}",
|
||||
"automatic_backup_from": "Copia de seguridad automática de {{date}}",
|
||||
"enable_automatic_cloud_sync": "Habilitar sincronización automática en la nube",
|
||||
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
|
||||
"clear": "Limpiar",
|
||||
"no_directory_selected": "No se seleccionó un directorio",
|
||||
@@ -185,7 +187,13 @@
|
||||
"reset_achievements_description": "Esto reiniciará todos los logros de {{game}}",
|
||||
"reset_achievements_title": "¿Estás seguro?",
|
||||
"reset_achievements_success": "Logros reiniciados exitosamente",
|
||||
"reset_achievements_error": "Se produjo un error al reiniciar los logros"
|
||||
"reset_achievements_error": "Se produjo un error al reiniciar los logros",
|
||||
"download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de Gofile. Por favor espera a que se reinicie la cuota.",
|
||||
"download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nueva descargas. Por favor, revisa los ajustes de tu cuenta e intenta de nuevo.",
|
||||
"download_error_not_cached_in_real_debrid": "Esta descarga no está disponible en Real-Debrid y el estado de descarga del sondeo de Real-Debrid aún no está disponible.",
|
||||
"download_error_not_cached_in_torbox": "Esta descarga no está disponible en Torbox y el estado de descarga del sondeo aún no está disponible.",
|
||||
"game_added_to_favorites": "Juego añadido a favoritos",
|
||||
"game_removed_from_favorites": "Juego removido de favoritos"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activar Hydra",
|
||||
@@ -259,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",
|
||||
@@ -290,14 +304,45 @@
|
||||
"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}}",
|
||||
"subscription_renew_cancelled": "Está desactivada la renovación automática",
|
||||
"subscription_renews_on": "Tú suscripción se renueva el {{date}}",
|
||||
"update_email": "Actualizar correo",
|
||||
"update_password": "Actualizar contraseña"
|
||||
"update_password": "Actualizar contraseña",
|
||||
"appearance": "Apariencia",
|
||||
"become_subscriber": "Sé Hydra Cloud",
|
||||
"cancel": "Cancelar",
|
||||
"clear_themes": "Limpiar",
|
||||
"create_theme": "Crear",
|
||||
"create_theme_modal_description": "Crea un nuevo tema para personalizar la apariencia de Hydra",
|
||||
"create_theme_modal_title": "Crear tema personalizado",
|
||||
"delete_all_themes": "Eliminar todos los temas",
|
||||
"delete_all_themes_description": "Esto eliminará todos tus temas personalizados",
|
||||
"delete_theme": "Eliminar tema",
|
||||
"delete_theme_description": "Esto eliminará el tema {{theme}}",
|
||||
"edit_theme": "Editar tema",
|
||||
"editor_tab_code": "Código",
|
||||
"editor_tab_info": "Info",
|
||||
"editor_tab_save": "Guardar",
|
||||
"enable_torbox": "Habilitar Torbox",
|
||||
"error_importing_theme": "Error al importar el tema",
|
||||
"import_theme": "Importar tema",
|
||||
"import_theme_description": "Vas a importar el tema {{theme}} desde la tienda de temas",
|
||||
"insert_theme_name": "Introducí el nombre del tema",
|
||||
"name_min_length": "El tema tiene que tener 3 carácteres de largo mínimo",
|
||||
"no_themes": "Parece que no tenés ningún tema aún, pero no te preocupes, presiona acá para crear tu primer tema.",
|
||||
"real_debrid_account_linked": "Cuenta de Real-Debrid vinculada",
|
||||
"set_theme": "Establecer tema",
|
||||
"theme_imported": "Tema importado exitosamente",
|
||||
"theme_name": "Nombre",
|
||||
"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",
|
||||
"enable_friend_request_notifications": "Cuando se recibe una solicitud de amistad",
|
||||
"enable_auto_install": "Descargar actualizaciones automáticamente"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Descarga completada",
|
||||
@@ -308,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",
|
||||
@@ -165,6 +167,8 @@
|
||||
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
|
||||
"achievements_not_sync": "Veja como exibir suas conquistas no perfil",
|
||||
"backup_from": "Backup de {{date}}",
|
||||
"automatic_backup_from": "Backup automático de {{date}}",
|
||||
"enable_automatic_cloud_sync": "Habilitar 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",
|
||||
@@ -181,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",
|
||||
@@ -218,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",
|
||||
@@ -255,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",
|
||||
@@ -315,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",
|
||||
@@ -329,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": {
|
||||
@@ -466,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}} результатов",
|
||||
@@ -178,12 +181,23 @@
|
||||
"manage_files_description": "Управляйте файлами, которые будут сохраняться и восстанавливаться",
|
||||
"select_folder": "Выбрать папку",
|
||||
"backup_from": "Резервная копия от {{date}}",
|
||||
"automatic_backup_from": "Автоматическая резервная копия от {{date}}",
|
||||
"enable_automatic_cloud_sync": "Включить автоматическую синхронизацию в облаке",
|
||||
"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": "Не удалось сбросить достижения"
|
||||
"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 пока невозможно.",
|
||||
"game_added_to_favorites": "Игра добавлена в избранное",
|
||||
"game_removed_from_favorites": "Игра удалена из избранного",
|
||||
"automatically_extract_downloaded_files": "Автоматическая распаковка загруженных файлов"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Активировать Hydra",
|
||||
@@ -220,7 +234,9 @@
|
||||
"seeding": "Раздача",
|
||||
"stop_seeding": "Остановить раздачу",
|
||||
"resume_seeding": "Продолжить раздачу",
|
||||
"options": "Управлять"
|
||||
"options": "Управлять",
|
||||
"extract": "Распаковать файлы",
|
||||
"extracting": "Распаковка файлов…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Путь загрузок",
|
||||
@@ -259,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}} вариант загрузки",
|
||||
@@ -269,7 +291,7 @@
|
||||
"import": "Импортировать",
|
||||
"blocked_users": "Заблокированные пользователи",
|
||||
"friends_only": "Только для друзей",
|
||||
"must_be_valid_url": "Источник должен быть действительным URL-адресом.",
|
||||
"must_be_valid_url": "У источника должен быть правильный URL",
|
||||
"privacy": "Конфиденциальность",
|
||||
"private": "Частный",
|
||||
"profile_visibility": "Видимость профиля",
|
||||
@@ -295,7 +317,45 @@
|
||||
"become_subscriber": "Станьте обладателем Hydra Cloud",
|
||||
"subscription_renew_cancelled": "Автоматическое продление отключено",
|
||||
"subscription_renews_on": "Ваша подписка продлевается на {{date}}",
|
||||
"bill_sent_until": "Ваш следующий счет будет отправлен до этого дня"
|
||||
"bill_sent_until": "Ваш следующий счет будет отправлен до этого дня",
|
||||
"no_themes": "Похоже, что у вас еще нет тем, но не волнуйтесь, нажмите здесь, чтобы создать свой первый шедевр",
|
||||
"editor_tab_code": "Код",
|
||||
"editor_tab_info": "Информация",
|
||||
"editor_tab_save": "Сохранить",
|
||||
"web_store": "Веб-магазин",
|
||||
"clear_themes": "Очистить",
|
||||
"create_theme": "Создать",
|
||||
"create_theme_modal_title": "Создать пользовательскую тему",
|
||||
"create_theme_modal_description": "Создать новую тему для настройки внешнего вида Hydra",
|
||||
"theme_name": "Название",
|
||||
"insert_theme_name": "Вставить название темы",
|
||||
"set_theme": "Установить тему",
|
||||
"unset_theme": "Снять тему",
|
||||
"delete_theme": "Удалить тему",
|
||||
"edit_theme": "Редактировать тему",
|
||||
"delete_all_themes": "Удалить все темы",
|
||||
"delete_all_themes_description": "Это удалит все ваши пользовательские темы",
|
||||
"delete_theme_description": "Это приведет к удалению темы {{theme}}",
|
||||
"cancel": "Отменить",
|
||||
"appearance": "Внешний вид",
|
||||
"enable_torbox": "Включить Torbox",
|
||||
"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": "Тема успешно импортирована",
|
||||
"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": "Загрузка завершена",
|
||||
@@ -306,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": {
|
||||
@@ -397,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": "Показывать заработанные очки в своем профиле"
|
||||
},
|
||||
@@ -431,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,44 +1,8 @@
|
||||
import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services";
|
||||
import { CloudSync } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import * as tar from "tar";
|
||||
import crypto from "node:crypto";
|
||||
import type { GameShop } from "@types";
|
||||
import axios from "axios";
|
||||
import os from "node:os";
|
||||
import { backupsPath } from "@main/constants";
|
||||
import { app } from "electron";
|
||||
import { normalizePath } from "@main/helpers";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const bundleBackup = async (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
winePrefix: string | null
|
||||
) => {
|
||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
||||
|
||||
// Remove existing backup
|
||||
if (fs.existsSync(backupPath)) {
|
||||
fs.rmSync(backupPath, { recursive: true });
|
||||
}
|
||||
|
||||
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
|
||||
|
||||
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
|
||||
|
||||
await tar.create(
|
||||
{
|
||||
gzip: false,
|
||||
file: tarLocation,
|
||||
cwd: backupPath,
|
||||
},
|
||||
["."]
|
||||
);
|
||||
|
||||
return tarLocation;
|
||||
};
|
||||
import i18next, { t } from "i18next";
|
||||
import { formatDate } from "date-fns";
|
||||
|
||||
const uploadSaveGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -46,61 +10,17 @@ const uploadSaveGame = async (
|
||||
shop: GameShop,
|
||||
downloadOptionTitle: string | null
|
||||
) => {
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
const { language } = i18next;
|
||||
|
||||
const bundleLocation = await bundleBackup(
|
||||
shop,
|
||||
return CloudSync.uploadSaveGame(
|
||||
objectId,
|
||||
game?.winePrefixPath ?? null
|
||||
shop,
|
||||
downloadOptionTitle,
|
||||
t("backup_from", {
|
||||
ns: "game_details",
|
||||
date: formatDate(new Date(), language),
|
||||
})
|
||||
);
|
||||
|
||||
fs.stat(bundleLocation, async (err, stat) => {
|
||||
if (err) {
|
||||
logger.error("Failed to get zip file stats", err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { uploadUrl } = await HydraApi.post<{
|
||||
id: string;
|
||||
uploadUrl: string;
|
||||
}>("/profile/games/artifacts", {
|
||||
artifactLengthInBytes: stat.size,
|
||||
shop,
|
||||
objectId,
|
||||
hostname: os.hostname(),
|
||||
homeDir: normalizePath(app.getPath("home")),
|
||||
downloadOptionTitle,
|
||||
platform: os.platform(),
|
||||
});
|
||||
|
||||
fs.readFile(bundleLocation, async (err, fileBuffer) => {
|
||||
if (err) {
|
||||
logger.error("Failed to read zip file", err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
await axios.put(uploadUrl, fileBuffer, {
|
||||
headers: {
|
||||
"Content-Type": "application/tar",
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
logger.log(progressEvent);
|
||||
},
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-upload-complete-${objectId}-${shop}`,
|
||||
true
|
||||
);
|
||||
|
||||
fs.rm(bundleLocation, (err) => {
|
||||
if (err) {
|
||||
logger.error("Failed to remove tar file", err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("uploadSaveGame", uploadSaveGame);
|
||||
|
||||
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";
|
||||
@@ -31,17 +32,22 @@ import "./library/remove-game";
|
||||
import "./library/remove-game-from-library";
|
||||
import "./library/select-game-wine-prefix";
|
||||
import "./library/reset-game-achievements";
|
||||
import "./library/toggle-automatic-cloud-sync";
|
||||
import "./misc/open-checkout";
|
||||
import "./misc/open-external";
|
||||
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";
|
||||
@@ -58,6 +64,7 @@ import "./user/get-blocked-users";
|
||||
import "./user/block-user";
|
||||
import "./user/unblock-user";
|
||||
import "./user/get-user-friends";
|
||||
import "./user/get-auth";
|
||||
import "./user/get-user-stats";
|
||||
import "./user/report-user";
|
||||
import "./user/get-unlocked-achievements";
|
||||
@@ -87,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);
|
||||
23
src/main/events/library/toggle-automatic-cloud-sync.ts
Normal file
23
src/main/events/library/toggle-automatic-cloud-sync.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { levelKeys, gamesSublevel } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const toggleAutomaticCloudSync = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
automaticCloudSync: boolean
|
||||
) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
if (!game) return;
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
automaticCloudSync,
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("toggleAutomaticCloudSync", toggleAutomaticCloudSync);
|
||||
@@ -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);
|
||||
22
src/main/events/misc/get-badges.ts
Normal file
22
src/main/events/misc/get-badges.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Badge } from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
|
||||
const getBadges = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const language = await db
|
||||
.get<string, string>(levelKeys.language, {
|
||||
valueEncoding: "utf-8",
|
||||
})
|
||||
.then((language) => language || "en");
|
||||
|
||||
const params = new URLSearchParams({
|
||||
locale: language,
|
||||
});
|
||||
|
||||
return HydraApi.get<Badge[]>(`/badges?${params.toString()}`, null, {
|
||||
needsAuth: false,
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("getBadges", getBadges);
|
||||
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,
|
||||
{
|
||||
|
||||
11
src/main/events/user/get-auth.ts
Normal file
11
src/main/events/user/get-auth.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import type { Auth } from "@types";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getAuth = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||
db.get<string, Auth>(levelKeys.auth, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
|
||||
registerEvent("getAuth", getAuth);
|
||||
@@ -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(
|
||||
@@ -62,141 +60,9 @@ export const loadState = async () => {
|
||||
game.uri !== null
|
||||
);
|
||||
|
||||
console.log("downloadsToSeed", downloadsToSeed);
|
||||
|
||||
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(levelKeys.language, rest.language);
|
||||
}
|
||||
}
|
||||
})
|
||||
.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();
|
||||
}
|
||||
}
|
||||
111
src/main/services/cloud-sync.ts
Normal file
111
src/main/services/cloud-sync.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { levelKeys, gamesSublevel, db } from "@main/level";
|
||||
import { app } from "electron";
|
||||
import path from "node:path";
|
||||
import * as tar from "tar";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import type { GameShop, User } from "@types";
|
||||
import { backupsPath } from "@main/constants";
|
||||
import { HydraApi } from "./hydra-api";
|
||||
import { normalizePath } from "@main/helpers";
|
||||
import { logger } from "./logger";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import axios from "axios";
|
||||
import { Ludusavi } from "./ludusavi";
|
||||
import { SubscriptionRequiredError } from "@shared";
|
||||
|
||||
export class CloudSync {
|
||||
private static async bundleBackup(
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
winePrefix: string | null
|
||||
) {
|
||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
||||
|
||||
// Remove existing backup
|
||||
if (fs.existsSync(backupPath)) {
|
||||
fs.rmSync(backupPath, { recursive: true });
|
||||
}
|
||||
|
||||
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
|
||||
|
||||
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
|
||||
|
||||
await tar.create(
|
||||
{
|
||||
gzip: false,
|
||||
file: tarLocation,
|
||||
cwd: backupPath,
|
||||
},
|
||||
["."]
|
||||
);
|
||||
|
||||
return tarLocation;
|
||||
}
|
||||
|
||||
public static async uploadSaveGame(
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
downloadOptionTitle: string | null,
|
||||
label?: string
|
||||
) {
|
||||
const hasActiveSubscription = await db
|
||||
.get<string, User>(levelKeys.user, { valueEncoding: "json" })
|
||||
.then((user) => {
|
||||
const expiresAt = new Date(user?.subscription?.expiresAt ?? 0);
|
||||
return expiresAt > new Date();
|
||||
});
|
||||
|
||||
if (!hasActiveSubscription) {
|
||||
throw new SubscriptionRequiredError();
|
||||
}
|
||||
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
const bundleLocation = await this.bundleBackup(
|
||||
shop,
|
||||
objectId,
|
||||
game?.winePrefixPath ?? null
|
||||
);
|
||||
|
||||
const stat = await fs.promises.stat(bundleLocation);
|
||||
|
||||
const { uploadUrl } = await HydraApi.post<{
|
||||
id: string;
|
||||
uploadUrl: string;
|
||||
}>("/profile/games/artifacts", {
|
||||
artifactLengthInBytes: stat.size,
|
||||
shop,
|
||||
objectId,
|
||||
hostname: os.hostname(),
|
||||
homeDir: normalizePath(app.getPath("home")),
|
||||
downloadOptionTitle,
|
||||
platform: os.platform(),
|
||||
label,
|
||||
});
|
||||
|
||||
const fileBuffer = await fs.promises.readFile(bundleLocation);
|
||||
|
||||
await axios.put(uploadUrl, fileBuffer, {
|
||||
headers: {
|
||||
"Content-Type": "application/tar",
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
logger.log(progressEvent);
|
||||
},
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-upload-complete-${objectId}-${shop}`,
|
||||
true
|
||||
);
|
||||
|
||||
fs.rm(bundleLocation, (err) => {
|
||||
if (err) {
|
||||
logger.error("Failed to remove tar file", err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
TorBoxAddTorrentRequest,
|
||||
TorBoxRequestLinkRequest,
|
||||
} from "@types";
|
||||
import { appVersion } from "@main/constants";
|
||||
|
||||
export class TorBoxClient {
|
||||
private static instance: AxiosInstance;
|
||||
@@ -18,6 +19,7 @@ export class TorBoxClient {
|
||||
baseURL: this.baseURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"User-Agent": `Hydra/${appVersion}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user