mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
Merge 24129481bc
into 110131f1d6
This commit is contained in:
commit
6ed43bc14f
27 changed files with 818 additions and 27 deletions
|
@ -44,5 +44,5 @@ class HttpDownloader:
|
||||||
'status': download.status,
|
'status': download.status,
|
||||||
'bytesDownloaded': download.completed_length,
|
'bytesDownloaded': download.completed_length,
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
151
python_rpc/http_multi_link_downloader.py
Normal file
151
python_rpc/http_multi_link_downloader.py
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
import aria2p
|
||||||
|
from aria2p.client import ClientException as DownloadNotFound
|
||||||
|
|
||||||
|
class HttpMultiLinkDownloader:
|
||||||
|
def __init__(self):
|
||||||
|
self.downloads = []
|
||||||
|
self.completed_downloads = []
|
||||||
|
self.total_size = None
|
||||||
|
self.aria2 = aria2p.API(
|
||||||
|
aria2p.Client(
|
||||||
|
host="http://localhost",
|
||||||
|
port=6800,
|
||||||
|
secret=""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def start_download(self, urls: list[str], save_path: str, header: str = None, out: str = None, total_size: int = None):
|
||||||
|
"""Add multiple URLs to download queue with same options"""
|
||||||
|
options = {"dir": save_path}
|
||||||
|
if header:
|
||||||
|
options["header"] = header
|
||||||
|
if out:
|
||||||
|
options["out"] = out
|
||||||
|
|
||||||
|
# Clear any existing downloads first
|
||||||
|
self.cancel_download()
|
||||||
|
self.completed_downloads = []
|
||||||
|
self.total_size = total_size
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
try:
|
||||||
|
added_downloads = self.aria2.add(url, options=options)
|
||||||
|
self.downloads.extend(added_downloads)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error adding download for URL {url}: {str(e)}")
|
||||||
|
|
||||||
|
def pause_download(self):
|
||||||
|
"""Pause all active downloads"""
|
||||||
|
if self.downloads:
|
||||||
|
try:
|
||||||
|
self.aria2.pause(self.downloads)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error pausing downloads: {str(e)}")
|
||||||
|
|
||||||
|
def cancel_download(self):
|
||||||
|
"""Cancel and remove all downloads"""
|
||||||
|
if self.downloads:
|
||||||
|
try:
|
||||||
|
# First try to stop the downloads
|
||||||
|
self.aria2.remove(self.downloads)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error removing downloads: {str(e)}")
|
||||||
|
finally:
|
||||||
|
# Clear the downloads list regardless of success/failure
|
||||||
|
self.downloads = []
|
||||||
|
self.completed_downloads = []
|
||||||
|
|
||||||
|
def get_download_status(self):
|
||||||
|
"""Get status for all tracked downloads, auto-remove completed/failed ones"""
|
||||||
|
if not self.downloads and not self.completed_downloads:
|
||||||
|
return []
|
||||||
|
|
||||||
|
total_completed = 0
|
||||||
|
current_download_speed = 0
|
||||||
|
active_downloads = []
|
||||||
|
to_remove = []
|
||||||
|
|
||||||
|
# First calculate sizes from completed downloads
|
||||||
|
for completed in self.completed_downloads:
|
||||||
|
total_completed += completed['size']
|
||||||
|
|
||||||
|
# Then check active downloads
|
||||||
|
for download in self.downloads:
|
||||||
|
try:
|
||||||
|
current_download = self.aria2.get_download(download.gid)
|
||||||
|
|
||||||
|
# Skip downloads that are not properly initialized
|
||||||
|
if not current_download or not current_download.files:
|
||||||
|
to_remove.append(download)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add to completed size and speed calculations
|
||||||
|
total_completed += current_download.completed_length
|
||||||
|
current_download_speed += current_download.download_speed
|
||||||
|
|
||||||
|
# If download is complete, move it to completed_downloads
|
||||||
|
if current_download.status == 'complete':
|
||||||
|
self.completed_downloads.append({
|
||||||
|
'name': current_download.name,
|
||||||
|
'size': current_download.total_length
|
||||||
|
})
|
||||||
|
to_remove.append(download)
|
||||||
|
else:
|
||||||
|
active_downloads.append({
|
||||||
|
'name': current_download.name,
|
||||||
|
'size': current_download.total_length,
|
||||||
|
'completed': current_download.completed_length,
|
||||||
|
'speed': current_download.download_speed
|
||||||
|
})
|
||||||
|
|
||||||
|
except DownloadNotFound:
|
||||||
|
to_remove.append(download)
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting download status: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Clean up completed/removed downloads from active list
|
||||||
|
for download in to_remove:
|
||||||
|
try:
|
||||||
|
if download in self.downloads:
|
||||||
|
self.downloads.remove(download)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Return aggregate status
|
||||||
|
if self.total_size or active_downloads or self.completed_downloads:
|
||||||
|
# Use the first active download's name as the folder name, or completed if none active
|
||||||
|
folder_name = None
|
||||||
|
if active_downloads:
|
||||||
|
folder_name = active_downloads[0]['name']
|
||||||
|
elif self.completed_downloads:
|
||||||
|
folder_name = self.completed_downloads[0]['name']
|
||||||
|
|
||||||
|
if folder_name and '/' in folder_name:
|
||||||
|
folder_name = folder_name.split('/')[0]
|
||||||
|
|
||||||
|
# Use provided total size if available, otherwise sum from downloads
|
||||||
|
total_size = self.total_size
|
||||||
|
if not total_size:
|
||||||
|
total_size = sum(d['size'] for d in active_downloads) + sum(d['size'] for d in self.completed_downloads)
|
||||||
|
|
||||||
|
# Calculate completion status based on total downloaded vs total size
|
||||||
|
is_complete = len(active_downloads) == 0 and total_completed >= (total_size * 0.99) # Allow 1% margin for size differences
|
||||||
|
|
||||||
|
# If all downloads are complete, clear the completed_downloads list to prevent status updates
|
||||||
|
if is_complete:
|
||||||
|
self.completed_downloads = []
|
||||||
|
|
||||||
|
return [{
|
||||||
|
'folderName': folder_name,
|
||||||
|
'fileSize': total_size,
|
||||||
|
'progress': total_completed / total_size if total_size > 0 else 0,
|
||||||
|
'downloadSpeed': current_download_speed,
|
||||||
|
'numPeers': 0,
|
||||||
|
'numSeeds': 0,
|
||||||
|
'status': 'complete' if is_complete else 'active',
|
||||||
|
'bytesDownloaded': total_completed,
|
||||||
|
}]
|
||||||
|
|
||||||
|
return []
|
|
@ -2,6 +2,7 @@ from flask import Flask, request, jsonify
|
||||||
import sys, json, urllib.parse, psutil
|
import sys, json, urllib.parse, psutil
|
||||||
from torrent_downloader import TorrentDownloader
|
from torrent_downloader import TorrentDownloader
|
||||||
from http_downloader import HttpDownloader
|
from http_downloader import HttpDownloader
|
||||||
|
from http_multi_link_downloader import HttpMultiLinkDownloader
|
||||||
from profile_image_processor import ProfileImageProcessor
|
from profile_image_processor import ProfileImageProcessor
|
||||||
import libtorrent as lt
|
import libtorrent as lt
|
||||||
|
|
||||||
|
@ -24,7 +25,15 @@ if start_download_payload:
|
||||||
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
|
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
|
||||||
downloading_game_id = initial_download['game_id']
|
downloading_game_id = initial_download['game_id']
|
||||||
|
|
||||||
if initial_download['url'].startswith('magnet'):
|
if isinstance(initial_download['url'], list):
|
||||||
|
# Handle multiple URLs using HttpMultiLinkDownloader
|
||||||
|
http_multi_downloader = HttpMultiLinkDownloader()
|
||||||
|
downloads[initial_download['game_id']] = http_multi_downloader
|
||||||
|
try:
|
||||||
|
http_multi_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
|
||||||
|
except Exception as e:
|
||||||
|
print("Error starting multi-link download", e)
|
||||||
|
elif initial_download['url'].startswith('magnet'):
|
||||||
torrent_downloader = TorrentDownloader(torrent_session)
|
torrent_downloader = TorrentDownloader(torrent_session)
|
||||||
downloads[initial_download['game_id']] = torrent_downloader
|
downloads[initial_download['game_id']] = torrent_downloader
|
||||||
try:
|
try:
|
||||||
|
@ -62,12 +71,23 @@ def status():
|
||||||
return auth_error
|
return auth_error
|
||||||
|
|
||||||
downloader = downloads.get(downloading_game_id)
|
downloader = downloads.get(downloading_game_id)
|
||||||
if downloader:
|
if not downloader:
|
||||||
status = downloads.get(downloading_game_id).get_download_status()
|
|
||||||
return jsonify(status), 200
|
|
||||||
else:
|
|
||||||
return jsonify(None)
|
return jsonify(None)
|
||||||
|
|
||||||
|
status = downloader.get_download_status()
|
||||||
|
if not status:
|
||||||
|
return jsonify(None)
|
||||||
|
|
||||||
|
if isinstance(status, list):
|
||||||
|
if not status: # Empty list
|
||||||
|
return jsonify(None)
|
||||||
|
|
||||||
|
# For multi-link downloader, use the aggregated status
|
||||||
|
# The status will already be aggregated by the HttpMultiLinkDownloader
|
||||||
|
return jsonify(status[0]), 200
|
||||||
|
|
||||||
|
return jsonify(status), 200
|
||||||
|
|
||||||
@app.route("/seed-status", methods=["GET"])
|
@app.route("/seed-status", methods=["GET"])
|
||||||
def seed_status():
|
def seed_status():
|
||||||
auth_error = validate_rpc_password()
|
auth_error = validate_rpc_password()
|
||||||
|
@ -81,10 +101,24 @@ def seed_status():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
response = downloader.get_download_status()
|
response = downloader.get_download_status()
|
||||||
if response is None:
|
if not response:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if response.get('status') == 5:
|
if isinstance(response, list):
|
||||||
|
# For multi-link downloader, check if all files are complete
|
||||||
|
if response and all(item['status'] == 'complete' for item in response):
|
||||||
|
seed_status.append({
|
||||||
|
'gameId': game_id,
|
||||||
|
'status': 'complete',
|
||||||
|
'folderName': response[0]['folderName'],
|
||||||
|
'fileSize': sum(item['fileSize'] for item in response),
|
||||||
|
'bytesDownloaded': sum(item['bytesDownloaded'] for item in response),
|
||||||
|
'downloadSpeed': 0,
|
||||||
|
'numPeers': 0,
|
||||||
|
'numSeeds': 0,
|
||||||
|
'progress': 1.0
|
||||||
|
})
|
||||||
|
elif response.get('status') == 5: # Original torrent seeding check
|
||||||
seed_status.append({
|
seed_status.append({
|
||||||
'gameId': game_id,
|
'gameId': game_id,
|
||||||
**response,
|
**response,
|
||||||
|
@ -138,7 +172,15 @@ def action():
|
||||||
|
|
||||||
existing_downloader = downloads.get(game_id)
|
existing_downloader = downloads.get(game_id)
|
||||||
|
|
||||||
if url.startswith('magnet'):
|
if isinstance(url, list):
|
||||||
|
# Handle multiple URLs using HttpMultiLinkDownloader
|
||||||
|
if existing_downloader and isinstance(existing_downloader, HttpMultiLinkDownloader):
|
||||||
|
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||||
|
else:
|
||||||
|
http_multi_downloader = HttpMultiLinkDownloader()
|
||||||
|
downloads[game_id] = http_multi_downloader
|
||||||
|
http_multi_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
|
||||||
|
elif url.startswith('magnet'):
|
||||||
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
||||||
existing_downloader.start_download(url, data['save_path'])
|
existing_downloader.start_download(url, data['save_path'])
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -227,7 +227,8 @@
|
||||||
"seeding": "Seeding",
|
"seeding": "Seeding",
|
||||||
"stop_seeding": "Stop seeding",
|
"stop_seeding": "Stop seeding",
|
||||||
"resume_seeding": "Resume seeding",
|
"resume_seeding": "Resume seeding",
|
||||||
"options": "Manage"
|
"options": "Manage",
|
||||||
|
"alldebrid_size_not_supported": "Download info for AllDebrid is not supported yet"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Downloads path",
|
"downloads_path": "Downloads path",
|
||||||
|
@ -306,7 +307,18 @@
|
||||||
"enable_torbox": "Enable Torbox",
|
"enable_torbox": "Enable Torbox",
|
||||||
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
|
||||||
"torbox_account_linked": "TorBox account linked",
|
"torbox_account_linked": "TorBox account linked",
|
||||||
"real_debrid_account_linked": "Real-Debrid account linked"
|
"real_debrid_account_linked": "Real-Debrid account linked",
|
||||||
|
"enable_all_debrid": "Enable All-Debrid",
|
||||||
|
"all_debrid_description": "All-Debrid is an unrestricted downloader that allows you to quickly download files from various sources.",
|
||||||
|
"all_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to All-Debrid",
|
||||||
|
"all_debrid_account_linked": "All-Debrid account linked successfully",
|
||||||
|
"alldebrid_missing_key": "Please provide an API key",
|
||||||
|
"alldebrid_invalid_key": "Invalid API key",
|
||||||
|
"alldebrid_blocked": "Your API key is geo-blocked or IP-blocked",
|
||||||
|
"alldebrid_banned": "This account has been banned",
|
||||||
|
"alldebrid_unknown_error": "An unknown error occurred",
|
||||||
|
"alldebrid_invalid_response": "Invalid response from All-Debrid",
|
||||||
|
"alldebrid_network_error": "Network error. Please check your connection"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
|
|
|
@ -134,7 +134,11 @@
|
||||||
"real_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la Real-Debrid",
|
"real_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la Real-Debrid",
|
||||||
"debrid_linked_message": "Contul \"{{username}}\" a fost legat",
|
"debrid_linked_message": "Contul \"{{username}}\" a fost legat",
|
||||||
"save_changes": "Salvează modificările",
|
"save_changes": "Salvează modificările",
|
||||||
"changes_saved": "Modificările au fost salvate cu succes"
|
"changes_saved": "Modificările au fost salvate cu succes",
|
||||||
|
"enable_all_debrid": "Activează All-Debrid",
|
||||||
|
"all_debrid_description": "All-Debrid este un descărcător fără restricții care îți permite să descarci fișiere din diverse surse.",
|
||||||
|
"all_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la All-Debrid",
|
||||||
|
"all_debrid_account_linked": "Contul All-Debrid a fost conectat cu succes"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Descărcare completă",
|
"download_complete": "Descărcare completă",
|
||||||
|
|
|
@ -48,6 +48,7 @@ import "./user-preferences/auto-launch";
|
||||||
import "./autoupdater/check-for-updates";
|
import "./autoupdater/check-for-updates";
|
||||||
import "./autoupdater/restart-and-install-update";
|
import "./autoupdater/restart-and-install-update";
|
||||||
import "./user-preferences/authenticate-real-debrid";
|
import "./user-preferences/authenticate-real-debrid";
|
||||||
|
import "./user-preferences/authenticate-all-debrid";
|
||||||
import "./user-preferences/authenticate-torbox";
|
import "./user-preferences/authenticate-torbox";
|
||||||
import "./download-sources/put-download-source";
|
import "./download-sources/put-download-source";
|
||||||
import "./auth/sign-out";
|
import "./auth/sign-out";
|
||||||
|
|
18
src/main/events/user-preferences/authenticate-all-debrid.ts
Normal file
18
src/main/events/user-preferences/authenticate-all-debrid.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { AllDebridClient } from "@main/services/download/all-debrid";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const authenticateAllDebrid = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
apiKey: string
|
||||||
|
) => {
|
||||||
|
AllDebridClient.authorize(apiKey);
|
||||||
|
const result = await AllDebridClient.getUser();
|
||||||
|
|
||||||
|
if ("error_code" in result) {
|
||||||
|
return { error_code: result.error_code };
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.user;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("authenticateAllDebrid", authenticateAllDebrid);
|
|
@ -15,6 +15,12 @@ const getUserPreferences = async () =>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userPreferences?.allDebridApiKey) {
|
||||||
|
userPreferences.allDebridApiKey = Crypto.decrypt(
|
||||||
|
userPreferences.allDebridApiKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (userPreferences?.torBoxApiToken) {
|
if (userPreferences?.torBoxApiToken) {
|
||||||
userPreferences.torBoxApiToken = Crypto.decrypt(
|
userPreferences.torBoxApiToken = Crypto.decrypt(
|
||||||
userPreferences.torBoxApiToken
|
userPreferences.torBoxApiToken
|
||||||
|
|
|
@ -30,6 +30,10 @@ const updateUserPreferences = async (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preferences.allDebridApiKey) {
|
||||||
|
preferences.allDebridApiKey = Crypto.encrypt(preferences.allDebridApiKey);
|
||||||
|
}
|
||||||
|
|
||||||
if (preferences.torBoxApiToken) {
|
if (preferences.torBoxApiToken) {
|
||||||
preferences.torBoxApiToken = Crypto.encrypt(preferences.torBoxApiToken);
|
preferences.torBoxApiToken = Crypto.encrypt(preferences.torBoxApiToken);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
startMainLoop,
|
startMainLoop,
|
||||||
} from "./services";
|
} from "./services";
|
||||||
import { RealDebridClient } from "./services/download/real-debrid";
|
import { RealDebridClient } from "./services/download/real-debrid";
|
||||||
|
import { AllDebridClient } from "./services/download/all-debrid";
|
||||||
import { HydraApi } from "./services/hydra-api";
|
import { HydraApi } from "./services/hydra-api";
|
||||||
import { uploadGamesBatch } from "./services/library-sync";
|
import { uploadGamesBatch } from "./services/library-sync";
|
||||||
import { Aria2 } from "./services/aria2";
|
import { Aria2 } from "./services/aria2";
|
||||||
|
@ -43,6 +44,10 @@ export const loadState = async () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userPreferences?.allDebridApiKey) {
|
||||||
|
AllDebridClient.authorize(Crypto.decrypt(userPreferences.allDebridApiKey));
|
||||||
|
}
|
||||||
|
|
||||||
if (userPreferences?.torBoxApiToken) {
|
if (userPreferences?.torBoxApiToken) {
|
||||||
TorBoxClient.authorize(Crypto.decrypt(userPreferences.torBoxApiToken));
|
TorBoxClient.authorize(Crypto.decrypt(userPreferences.torBoxApiToken));
|
||||||
}
|
}
|
||||||
|
@ -117,7 +122,8 @@ const migrateFromSqlite = async () => {
|
||||||
.select("*")
|
.select("*")
|
||||||
.then(async (userPreferences) => {
|
.then(async (userPreferences) => {
|
||||||
if (userPreferences.length > 0) {
|
if (userPreferences.length > 0) {
|
||||||
const { realDebridApiToken, ...rest } = userPreferences[0];
|
const { realDebridApiToken, allDebridApiKey, ...rest } =
|
||||||
|
userPreferences[0];
|
||||||
|
|
||||||
await db.put<string, UserPreferences>(
|
await db.put<string, UserPreferences>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
|
@ -126,6 +132,9 @@ const migrateFromSqlite = async () => {
|
||||||
realDebridApiToken: realDebridApiToken
|
realDebridApiToken: realDebridApiToken
|
||||||
? Crypto.encrypt(realDebridApiToken)
|
? Crypto.encrypt(realDebridApiToken)
|
||||||
: null,
|
: null,
|
||||||
|
allDebridApiKey: allDebridApiKey
|
||||||
|
? Crypto.encrypt(allDebridApiKey)
|
||||||
|
: null,
|
||||||
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
||||||
runAtStartup: rest.runAtStartup === 1,
|
runAtStartup: rest.runAtStartup === 1,
|
||||||
startMinimized: rest.startMinimized === 1,
|
startMinimized: rest.startMinimized === 1,
|
||||||
|
|
313
src/main/services/download/all-debrid.ts
Normal file
313
src/main/services/download/all-debrid.ts
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
import axios, { AxiosInstance } from "axios";
|
||||||
|
import type { AllDebridUser } from "@types";
|
||||||
|
import { logger } from "@main/services";
|
||||||
|
|
||||||
|
interface AllDebridMagnetStatus {
|
||||||
|
id: number;
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
status: string;
|
||||||
|
statusCode: number;
|
||||||
|
downloaded: number;
|
||||||
|
uploaded: number;
|
||||||
|
seeders: number;
|
||||||
|
downloadSpeed: number;
|
||||||
|
uploadSpeed: number;
|
||||||
|
uploadDate: number;
|
||||||
|
completionDate: number;
|
||||||
|
links: Array<{
|
||||||
|
link: string;
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AllDebridError {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AllDebridDownloadUrl {
|
||||||
|
link: string;
|
||||||
|
size?: number;
|
||||||
|
filename?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AllDebridClient {
|
||||||
|
private static instance: AxiosInstance;
|
||||||
|
private static readonly baseURL = "https://api.alldebrid.com/v4";
|
||||||
|
|
||||||
|
static authorize(apiKey: string) {
|
||||||
|
logger.info("[AllDebrid] Authorizing with key:", apiKey ? "***" : "empty");
|
||||||
|
this.instance = axios.create({
|
||||||
|
baseURL: this.baseURL,
|
||||||
|
params: {
|
||||||
|
agent: "hydra",
|
||||||
|
apikey: apiKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getUser() {
|
||||||
|
try {
|
||||||
|
const response = await this.instance.get<{
|
||||||
|
status: string;
|
||||||
|
data?: { user: AllDebridUser };
|
||||||
|
error?: AllDebridError;
|
||||||
|
}>("/user");
|
||||||
|
|
||||||
|
logger.info("[AllDebrid] API Response:", response.data);
|
||||||
|
|
||||||
|
if (response.data.status === "error") {
|
||||||
|
const error = response.data.error;
|
||||||
|
logger.error("[AllDebrid] API Error:", error);
|
||||||
|
if (error?.code === "AUTH_MISSING_APIKEY") {
|
||||||
|
return { error_code: "alldebrid_missing_key" };
|
||||||
|
}
|
||||||
|
if (error?.code === "AUTH_BAD_APIKEY") {
|
||||||
|
return { error_code: "alldebrid_invalid_key" };
|
||||||
|
}
|
||||||
|
if (error?.code === "AUTH_BLOCKED") {
|
||||||
|
return { error_code: "alldebrid_blocked" };
|
||||||
|
}
|
||||||
|
if (error?.code === "AUTH_USER_BANNED") {
|
||||||
|
return { error_code: "alldebrid_banned" };
|
||||||
|
}
|
||||||
|
return { error_code: "alldebrid_unknown_error" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.data.data?.user) {
|
||||||
|
logger.error("[AllDebrid] No user data in response");
|
||||||
|
return { error_code: "alldebrid_invalid_response" };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Successfully got user:",
|
||||||
|
response.data.data.user.username
|
||||||
|
);
|
||||||
|
return { user: response.data.data.user };
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("[AllDebrid] Request Error:", error);
|
||||||
|
if (error.response?.data?.error) {
|
||||||
|
return { error_code: "alldebrid_invalid_key" };
|
||||||
|
}
|
||||||
|
return { error_code: "alldebrid_network_error" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async uploadMagnet(magnet: string) {
|
||||||
|
try {
|
||||||
|
logger.info("[AllDebrid] Uploading magnet with params:", { magnet });
|
||||||
|
|
||||||
|
const response = await this.instance.get("/magnet/upload", {
|
||||||
|
params: {
|
||||||
|
magnets: [magnet],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Upload Magnet Raw Response:",
|
||||||
|
JSON.stringify(response.data, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.status === "error") {
|
||||||
|
throw new Error(response.data.error?.message || "Unknown error");
|
||||||
|
}
|
||||||
|
|
||||||
|
const magnetInfo = response.data.data.magnets[0];
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Magnet Info:",
|
||||||
|
JSON.stringify(magnetInfo, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (magnetInfo.error) {
|
||||||
|
throw new Error(magnetInfo.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return magnetInfo.id;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("[AllDebrid] Upload Magnet Error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async checkMagnetStatus(
|
||||||
|
magnetId: number
|
||||||
|
): Promise<AllDebridMagnetStatus> {
|
||||||
|
try {
|
||||||
|
logger.info("[AllDebrid] Checking magnet status for ID:", magnetId);
|
||||||
|
|
||||||
|
const response = await this.instance.get(`/magnet/status`, {
|
||||||
|
params: {
|
||||||
|
id: magnetId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Check Magnet Status Raw Response:",
|
||||||
|
JSON.stringify(response.data, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.data) {
|
||||||
|
throw new Error("No response data received");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data.status === "error") {
|
||||||
|
throw new Error(response.data.error?.message || "Unknown error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificăm noua structură a răspunsului
|
||||||
|
const magnetData = response.data.data?.magnets;
|
||||||
|
if (!magnetData || typeof magnetData !== "object") {
|
||||||
|
logger.error(
|
||||||
|
"[AllDebrid] Invalid response structure:",
|
||||||
|
JSON.stringify(response.data, null, 2)
|
||||||
|
);
|
||||||
|
throw new Error("Invalid magnet status response format");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertim răspunsul în formatul așteptat
|
||||||
|
const magnetStatus: AllDebridMagnetStatus = {
|
||||||
|
id: magnetData.id,
|
||||||
|
filename: magnetData.filename,
|
||||||
|
size: magnetData.size,
|
||||||
|
status: magnetData.status,
|
||||||
|
statusCode: magnetData.statusCode,
|
||||||
|
downloaded: magnetData.downloaded,
|
||||||
|
uploaded: magnetData.uploaded,
|
||||||
|
seeders: magnetData.seeders,
|
||||||
|
downloadSpeed: magnetData.downloadSpeed,
|
||||||
|
uploadSpeed: magnetData.uploadSpeed,
|
||||||
|
uploadDate: magnetData.uploadDate,
|
||||||
|
completionDate: magnetData.completionDate,
|
||||||
|
links: magnetData.links.map((link) => ({
|
||||||
|
link: link.link,
|
||||||
|
filename: link.filename,
|
||||||
|
size: link.size,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Magnet Status:",
|
||||||
|
JSON.stringify(magnetStatus, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
return magnetStatus;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("[AllDebrid] Check Magnet Status Error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async unlockLink(link: string) {
|
||||||
|
try {
|
||||||
|
const response = await this.instance.get<{
|
||||||
|
status: string;
|
||||||
|
data?: { link: string };
|
||||||
|
error?: AllDebridError;
|
||||||
|
}>("/link/unlock", {
|
||||||
|
params: {
|
||||||
|
link,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.status === "error") {
|
||||||
|
throw new Error(response.data.error?.message || "Unknown error");
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlockedLink = response.data.data?.link;
|
||||||
|
if (!unlockedLink) {
|
||||||
|
throw new Error("No download link received from AllDebrid");
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlockedLink;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("[AllDebrid] Unlock Link Error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDownloadUrls(
|
||||||
|
uri: string
|
||||||
|
): Promise<AllDebridDownloadUrl[]> {
|
||||||
|
try {
|
||||||
|
logger.info("[AllDebrid] Getting download URLs for URI:", uri);
|
||||||
|
|
||||||
|
if (uri.startsWith("magnet:")) {
|
||||||
|
logger.info("[AllDebrid] Detected magnet link, uploading...");
|
||||||
|
// 1. Upload magnet
|
||||||
|
const magnetId = await this.uploadMagnet(uri);
|
||||||
|
logger.info("[AllDebrid] Magnet uploaded, ID:", magnetId);
|
||||||
|
|
||||||
|
// 2. Verificăm statusul până când avem link-uri
|
||||||
|
let retries = 0;
|
||||||
|
let magnetStatus: AllDebridMagnetStatus;
|
||||||
|
|
||||||
|
do {
|
||||||
|
magnetStatus = await this.checkMagnetStatus(magnetId);
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Magnet status:",
|
||||||
|
magnetStatus.status,
|
||||||
|
"statusCode:",
|
||||||
|
magnetStatus.statusCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (magnetStatus.statusCode === 4) {
|
||||||
|
// Ready
|
||||||
|
// Deblocăm fiecare link în parte și aruncăm eroare dacă oricare eșuează
|
||||||
|
const unlockedLinks = await Promise.all(
|
||||||
|
magnetStatus.links.map(async (link) => {
|
||||||
|
try {
|
||||||
|
const unlockedLink = await this.unlockLink(link.link);
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Successfully unlocked link:",
|
||||||
|
unlockedLink
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
link: unlockedLink,
|
||||||
|
size: link.size,
|
||||||
|
filename: link.filename,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"[AllDebrid] Failed to unlock link:",
|
||||||
|
link.link,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw new Error("Failed to unlock all links");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[AllDebrid] Got unlocked download links:",
|
||||||
|
unlockedLinks
|
||||||
|
);
|
||||||
|
return unlockedLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retries++ > 30) {
|
||||||
|
// Maximum 30 de încercări
|
||||||
|
throw new Error("Timeout waiting for magnet to be ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000)); // Așteptăm 2 secunde între verificări
|
||||||
|
} while (magnetStatus.statusCode !== 4);
|
||||||
|
} else {
|
||||||
|
logger.info("[AllDebrid] Regular link, unlocking...");
|
||||||
|
// Pentru link-uri normale, doar debridam link-ul
|
||||||
|
const downloadUrl = await this.unlockLink(uri);
|
||||||
|
logger.info("[AllDebrid] Got unlocked download URL:", downloadUrl);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
link: downloadUrl,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("[AllDebrid] Get Download URLs Error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return []; // Add default return for TypeScript
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import { logger } from "../logger";
|
||||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
import { TorBoxClient } from "./torbox";
|
import { TorBoxClient } from "./torbox";
|
||||||
|
import { AllDebridClient } from "./all-debrid";
|
||||||
|
|
||||||
export class DownloadManager {
|
export class DownloadManager {
|
||||||
private static downloadingGameId: string | null = null;
|
private static downloadingGameId: string | null = null;
|
||||||
|
@ -32,6 +33,7 @@ export class DownloadManager {
|
||||||
})
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
downloadsToSeed?.map((download) => ({
|
downloadsToSeed?.map((download) => ({
|
||||||
|
action: "seed",
|
||||||
game_id: levelKeys.game(download.shop, download.objectId),
|
game_id: levelKeys.game(download.shop, download.objectId),
|
||||||
url: download.uri,
|
url: download.uri,
|
||||||
save_path: download.downloadPath,
|
save_path: download.downloadPath,
|
||||||
|
@ -314,6 +316,27 @@ export class DownloadManager {
|
||||||
save_path: download.downloadPath,
|
save_path: download.downloadPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case Downloader.AllDebrid: {
|
||||||
|
const downloadUrls = await AllDebridClient.getDownloadUrls(
|
||||||
|
download.uri
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!downloadUrls.length)
|
||||||
|
throw new Error(DownloadError.NotCachedInAllDebrid);
|
||||||
|
|
||||||
|
const totalSize = downloadUrls.reduce(
|
||||||
|
(total, url) => total + (url.size || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
action: "start",
|
||||||
|
game_id: downloadId,
|
||||||
|
url: downloadUrls.map((d) => d.link),
|
||||||
|
save_path: download.downloadPath,
|
||||||
|
total_size: totalSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
case Downloader.Torrent:
|
case Downloader.Torrent:
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
|
|
|
@ -17,17 +17,24 @@ export const calculateETA = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDirSize = async (dir: string): Promise<number> => {
|
export const getDirSize = async (dir: string): Promise<number> => {
|
||||||
const getItemSize = async (filePath: string): Promise<number> => {
|
try {
|
||||||
const stat = await fs.promises.stat(filePath);
|
const stat = await fs.promises.stat(dir);
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
// If it's a file, return its size directly
|
||||||
return getDirSize(filePath);
|
if (!stat.isDirectory()) {
|
||||||
|
return stat.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
return stat.size;
|
const getItemSize = async (filePath: string): Promise<number> => {
|
||||||
};
|
const stat = await fs.promises.stat(filePath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
return getDirSize(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stat.size;
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
|
||||||
const files = await fs.promises.readdir(dir);
|
const files = await fs.promises.readdir(dir);
|
||||||
const filePaths = files.map((file) => path.join(dir, file));
|
const filePaths = files.map((file) => path.join(dir, file));
|
||||||
const sizes = await Promise.all(filePaths.map(getItemSize));
|
const sizes = await Promise.all(filePaths.map(getItemSize));
|
||||||
|
|
|
@ -10,9 +10,13 @@ import { Readable } from "node:stream";
|
||||||
import { app, dialog } from "electron";
|
import { app, dialog } from "electron";
|
||||||
|
|
||||||
interface GamePayload {
|
interface GamePayload {
|
||||||
|
action: string;
|
||||||
game_id: string;
|
game_id: string;
|
||||||
url: string;
|
url: string | string[];
|
||||||
save_path: string;
|
save_path: string;
|
||||||
|
header?: string;
|
||||||
|
out?: string;
|
||||||
|
total_size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
||||||
|
|
|
@ -92,6 +92,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
ipcRenderer.invoke("autoLaunch", autoLaunchProps),
|
ipcRenderer.invoke("autoLaunch", autoLaunchProps),
|
||||||
authenticateRealDebrid: (apiToken: string) =>
|
authenticateRealDebrid: (apiToken: string) =>
|
||||||
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
|
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
|
||||||
|
authenticateAllDebrid: (apiKey: string) =>
|
||||||
|
ipcRenderer.invoke("authenticateAllDebrid", apiKey),
|
||||||
authenticateTorBox: (apiToken: string) =>
|
authenticateTorBox: (apiToken: string) =>
|
||||||
ipcRenderer.invoke("authenticateTorBox", apiToken),
|
ipcRenderer.invoke("authenticateTorBox", apiToken),
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ export const DOWNLOADER_NAME = {
|
||||||
[Downloader.Datanodes]: "Datanodes",
|
[Downloader.Datanodes]: "Datanodes",
|
||||||
[Downloader.Mediafire]: "Mediafire",
|
[Downloader.Mediafire]: "Mediafire",
|
||||||
[Downloader.TorBox]: "TorBox",
|
[Downloader.TorBox]: "TorBox",
|
||||||
|
[Downloader.AllDebrid]: "All-Debrid",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||||
|
|
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
|
@ -29,6 +29,7 @@ import type {
|
||||||
LibraryGame,
|
LibraryGame,
|
||||||
GameRunning,
|
GameRunning,
|
||||||
TorBoxUser,
|
TorBoxUser,
|
||||||
|
AllDebridUser,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import type disk from "diskusage";
|
import type disk from "diskusage";
|
||||||
|
@ -150,6 +151,9 @@ declare global {
|
||||||
minimized: boolean;
|
minimized: boolean;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||||
|
authenticateAllDebrid: (
|
||||||
|
apiKey: string
|
||||||
|
) => Promise<AllDebridUser | { error_code: string }>;
|
||||||
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
||||||
onAchievementUnlocked: (cb: () => void) => () => Electron.IpcRenderer;
|
onAchievementUnlocked: (cb: () => void) => () => Electron.IpcRenderer;
|
||||||
|
|
||||||
|
|
|
@ -112,6 +112,15 @@ export function DownloadGroup({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (download.downloader === Downloader.AllDebrid) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>{progress}</p>
|
||||||
|
<p>{t("alldebrid_size_not_supported")}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>{progress}</p>
|
<p>{progress}</p>
|
||||||
|
@ -154,6 +163,15 @@ export function DownloadGroup({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (download.status === "active") {
|
if (download.status === "active") {
|
||||||
|
if (download.downloader === Downloader.AllDebrid) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>{formatDownloadProgress(download.progress)}</p>
|
||||||
|
<p>{t("alldebrid_size_not_supported")}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>{formatDownloadProgress(download.progress)}</p>
|
<p>{formatDownloadProgress(download.progress)}</p>
|
||||||
|
@ -240,7 +258,9 @@ export function DownloadGroup({
|
||||||
(download?.downloader === Downloader.RealDebrid &&
|
(download?.downloader === Downloader.RealDebrid &&
|
||||||
!userPreferences?.realDebridApiToken) ||
|
!userPreferences?.realDebridApiToken) ||
|
||||||
(download?.downloader === Downloader.TorBox &&
|
(download?.downloader === Downloader.TorBox &&
|
||||||
!userPreferences?.torBoxApiToken);
|
!userPreferences?.torBoxApiToken) ||
|
||||||
|
(download?.downloader === Downloader.AllDebrid &&
|
||||||
|
!userPreferences?.allDebridApiKey);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
@ -196,7 +196,6 @@ export function HeroPanelActions() {
|
||||||
{game.favorite ? <HeartFillIcon /> : <HeartIcon />}
|
{game.favorite ? <HeartFillIcon /> : <HeartIcon />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowGameOptionsModal(true)}
|
onClick={() => setShowGameOptionsModal(true)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
|
|
|
@ -87,6 +87,8 @@ export function DownloadSettingsModal({
|
||||||
return userPreferences?.realDebridApiToken;
|
return userPreferences?.realDebridApiToken;
|
||||||
if (downloader === Downloader.TorBox)
|
if (downloader === Downloader.TorBox)
|
||||||
return userPreferences?.torBoxApiToken;
|
return userPreferences?.torBoxApiToken;
|
||||||
|
if (downloader === Downloader.AllDebrid)
|
||||||
|
return userPreferences?.allDebridApiKey;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -100,6 +102,8 @@ export function DownloadSettingsModal({
|
||||||
userPreferences?.downloadsPath,
|
userPreferences?.downloadsPath,
|
||||||
downloaders,
|
downloaders,
|
||||||
userPreferences?.realDebridApiToken,
|
userPreferences?.realDebridApiToken,
|
||||||
|
userPreferences?.torBoxApiToken,
|
||||||
|
userPreferences?.allDebridApiKey,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleChooseDownloadsPath = async () => {
|
const handleChooseDownloadsPath = async () => {
|
||||||
|
@ -163,8 +167,12 @@ export function DownloadSettingsModal({
|
||||||
selectedDownloader === downloader ? "primary" : "outline"
|
selectedDownloader === downloader ? "primary" : "outline"
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
downloader === Downloader.RealDebrid &&
|
(downloader === Downloader.RealDebrid &&
|
||||||
!userPreferences?.realDebridApiToken
|
!userPreferences?.realDebridApiToken) ||
|
||||||
|
(downloader === Downloader.AllDebrid &&
|
||||||
|
!userPreferences?.allDebridApiKey) ||
|
||||||
|
(downloader === Downloader.TorBox &&
|
||||||
|
!userPreferences?.torBoxApiToken)
|
||||||
}
|
}
|
||||||
onClick={() => setSelectedDownloader(downloader)}
|
onClick={() => setSelectedDownloader(downloader)}
|
||||||
>
|
>
|
||||||
|
|
12
src/renderer/src/pages/settings/settings-all-debrid.scss
Normal file
12
src/renderer/src/pages/settings/settings-all-debrid.scss
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.settings-all-debrid {
|
||||||
|
&__form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
}
|
129
src/renderer/src/pages/settings/settings-all-debrid.tsx
Normal file
129
src/renderer/src/pages/settings/settings-all-debrid.tsx
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Button, CheckboxField, Link, TextField } from "@renderer/components";
|
||||||
|
import "./settings-all-debrid.scss";
|
||||||
|
|
||||||
|
import { useAppSelector, useToast } from "@renderer/hooks";
|
||||||
|
|
||||||
|
import { settingsContext } from "@renderer/context";
|
||||||
|
|
||||||
|
const ALL_DEBRID_API_TOKEN_URL = "https://alldebrid.com/apikeys";
|
||||||
|
|
||||||
|
export function SettingsAllDebrid() {
|
||||||
|
const userPreferences = useAppSelector(
|
||||||
|
(state) => state.userPreferences.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const { updateUserPreferences } = useContext(settingsContext);
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
useAllDebrid: false,
|
||||||
|
allDebridApiKey: null as string | null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userPreferences) {
|
||||||
|
setForm({
|
||||||
|
useAllDebrid: Boolean(userPreferences.allDebridApiKey),
|
||||||
|
allDebridApiKey: userPreferences.allDebridApiKey ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [userPreferences]);
|
||||||
|
|
||||||
|
const handleFormSubmit: React.FormEventHandler<HTMLFormElement> = async (
|
||||||
|
event
|
||||||
|
) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (form.useAllDebrid) {
|
||||||
|
if (!form.allDebridApiKey) {
|
||||||
|
showErrorToast(t("alldebrid_missing_key"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await window.electron.authenticateAllDebrid(
|
||||||
|
form.allDebridApiKey
|
||||||
|
);
|
||||||
|
|
||||||
|
if ("error_code" in result) {
|
||||||
|
showErrorToast(t(result.error_code));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.isPremium) {
|
||||||
|
showErrorToast(
|
||||||
|
t("all_debrid_free_account_error", { username: result.username })
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessToast(
|
||||||
|
t("all_debrid_account_linked"),
|
||||||
|
t("debrid_linked_message", { username: result.username })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showSuccessToast(t("changes_saved"));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUserPreferences({
|
||||||
|
allDebridApiKey: form.useAllDebrid ? form.allDebridApiKey : null,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
showErrorToast(t("alldebrid_unknown_error"));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isButtonDisabled =
|
||||||
|
(form.useAllDebrid && !form.allDebridApiKey) || isLoading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="settings-all-debrid__form" onSubmit={handleFormSubmit}>
|
||||||
|
<p className="settings-all-debrid__description">
|
||||||
|
{t("all_debrid_description")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<CheckboxField
|
||||||
|
label={t("enable_all_debrid")}
|
||||||
|
checked={form.useAllDebrid}
|
||||||
|
onChange={() =>
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
useAllDebrid: !form.useAllDebrid,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.useAllDebrid && (
|
||||||
|
<TextField
|
||||||
|
label={t("api_token")}
|
||||||
|
value={form.allDebridApiKey ?? ""}
|
||||||
|
type="password"
|
||||||
|
onChange={(event) =>
|
||||||
|
setForm({ ...form, allDebridApiKey: event.target.value })
|
||||||
|
}
|
||||||
|
rightContent={
|
||||||
|
<Button type="submit" disabled={isButtonDisabled}>
|
||||||
|
{t("save_changes")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
placeholder="API Key"
|
||||||
|
hint={
|
||||||
|
<Trans i18nKey="debrid_api_token_hint" ns="settings">
|
||||||
|
<Link to={ALL_DEBRID_API_TOKEN_URL} />
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { SettingsRealDebrid } from "./settings-real-debrid";
|
import { SettingsRealDebrid } from "./settings-real-debrid";
|
||||||
|
import { SettingsAllDebrid } from "./settings-all-debrid";
|
||||||
import { SettingsGeneral } from "./settings-general";
|
import { SettingsGeneral } from "./settings-general";
|
||||||
import { SettingsBehavior } from "./settings-behavior";
|
import { SettingsBehavior } from "./settings-behavior";
|
||||||
import torBoxLogo from "@renderer/assets/icons/torbox.webp";
|
import torBoxLogo from "@renderer/assets/icons/torbox.webp";
|
||||||
|
@ -35,6 +36,7 @@ export default function Settings() {
|
||||||
contentTitle: "TorBox",
|
contentTitle: "TorBox",
|
||||||
},
|
},
|
||||||
{ tabLabel: "Real-Debrid", contentTitle: "Real-Debrid" },
|
{ tabLabel: "Real-Debrid", contentTitle: "Real-Debrid" },
|
||||||
|
{ tabLabel: "All-Debrid", contentTitle: "All-Debrid" },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (userDetails)
|
if (userDetails)
|
||||||
|
@ -70,6 +72,10 @@ export default function Settings() {
|
||||||
return <SettingsRealDebrid />;
|
return <SettingsRealDebrid />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentCategoryIndex === 5) {
|
||||||
|
return <SettingsAllDebrid />;
|
||||||
|
}
|
||||||
|
|
||||||
return <SettingsAccount />;
|
return <SettingsAccount />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ export enum Downloader {
|
||||||
Datanodes,
|
Datanodes,
|
||||||
Mediafire,
|
Mediafire,
|
||||||
TorBox,
|
TorBox,
|
||||||
|
AllDebrid,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DownloadSourceStatus {
|
export enum DownloadSourceStatus {
|
||||||
|
@ -54,6 +55,7 @@ export enum AuthPage {
|
||||||
export enum DownloadError {
|
export enum DownloadError {
|
||||||
NotCachedInRealDebrid = "download_error_not_cached_in_real_debrid",
|
NotCachedInRealDebrid = "download_error_not_cached_in_real_debrid",
|
||||||
NotCachedInTorbox = "download_error_not_cached_in_torbox",
|
NotCachedInTorbox = "download_error_not_cached_in_torbox",
|
||||||
|
NotCachedInAllDebrid = "download_error_not_cached_in_alldebrid",
|
||||||
GofileQuotaExceeded = "download_error_gofile_quota_exceeded",
|
GofileQuotaExceeded = "download_error_gofile_quota_exceeded",
|
||||||
RealDebridAccountNotAuthorized = "download_error_real_debrid_account_not_authorized",
|
RealDebridAccountNotAuthorized = "download_error_real_debrid_account_not_authorized",
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,12 @@ export const getDownloadersForUri = (uri: string) => {
|
||||||
return [Downloader.RealDebrid];
|
return [Downloader.RealDebrid];
|
||||||
|
|
||||||
if (uri.startsWith("magnet:")) {
|
if (uri.startsWith("magnet:")) {
|
||||||
return [Downloader.Torrent, Downloader.TorBox, Downloader.RealDebrid];
|
return [
|
||||||
|
Downloader.Torrent,
|
||||||
|
Downloader.TorBox,
|
||||||
|
Downloader.RealDebrid,
|
||||||
|
Downloader.AllDebrid,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
|
|
@ -174,3 +174,11 @@ export interface SeedingStatus {
|
||||||
status: DownloadStatus;
|
status: DownloadStatus;
|
||||||
uploadSpeed: number;
|
uploadSpeed: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* All-Debrid */
|
||||||
|
export interface AllDebridUser {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
isPremium: boolean;
|
||||||
|
premiumUntil: string;
|
||||||
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ export interface UserPreferences {
|
||||||
downloadsPath?: string | null;
|
downloadsPath?: string | null;
|
||||||
language?: string;
|
language?: string;
|
||||||
realDebridApiToken?: string | null;
|
realDebridApiToken?: string | null;
|
||||||
|
allDebridApiKey?: string | null;
|
||||||
torBoxApiToken?: string | null;
|
torBoxApiToken?: string | null;
|
||||||
preferQuitInsteadOfHiding?: boolean;
|
preferQuitInsteadOfHiding?: boolean;
|
||||||
runAtStartup?: boolean;
|
runAtStartup?: boolean;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue