Update PR #1452 to latest HydraLauncher and fix conflicts

This commit is contained in:
ItsYeBoi20
2025-10-01 05:04:34 +02:00
parent 79498abdb5
commit aacf9abc6a
26 changed files with 805 additions and 179 deletions

View 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 []

View File

@@ -3,6 +3,7 @@ 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 profile_image_processor import ProfileImageProcessor from profile_image_processor import ProfileImageProcessor
from http_multi_link_downloader import HttpMultiLinkDownloader
import libtorrent as lt import libtorrent as lt
app = Flask(__name__) app = Flask(__name__)
@@ -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,
@@ -143,7 +177,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:

View File

@@ -4,6 +4,7 @@
"successfully_signed_in": "Successfully signed in" "successfully_signed_in": "Successfully signed in"
}, },
"home": { "home": {
"featured": "Featured",
"surprise_me": "Surprise me", "surprise_me": "Surprise me",
"no_results": "No results found", "no_results": "No results found",
"start_typing": "Starting typing to search...", "start_typing": "Starting typing to search...",
@@ -27,56 +28,7 @@
"friends": "Friends", "friends": "Friends",
"need_help": "Need help?", "need_help": "Need help?",
"favorites": "Favorites", "favorites": "Favorites",
"playable_button_title": "Show only games you can play now", "playable_button_title": "Show only games you can play now"
"add_custom_game_tooltip": "Add Custom Game",
"show_playable_only_tooltip": "Show Playable Only",
"custom_game_modal": "Add Custom Game",
"custom_game_modal_description": "Add a custom game to your library by selecting an executable file",
"custom_game_modal_executable_path": "Executable Path",
"custom_game_modal_select_executable": "Select executable file",
"custom_game_modal_title": "Title",
"custom_game_modal_enter_title": "Enter title",
"custom_game_modal_browse": "Browse",
"custom_game_modal_cancel": "Cancel",
"custom_game_modal_add": "Add Game",
"custom_game_modal_adding": "Adding Game...",
"custom_game_modal_success": "Custom game added successfully",
"custom_game_modal_failed": "Failed to add custom game",
"custom_game_modal_executable": "Executable",
"edit_game_modal": "Customize Assets",
"edit_game_modal_description": "Customize game assets and details",
"edit_game_modal_title": "Title",
"edit_game_modal_enter_title": "Enter title",
"edit_game_modal_image": "Image",
"edit_game_modal_select_image": "Select image",
"edit_game_modal_browse": "Browse",
"edit_game_modal_image_preview": "Image preview",
"edit_game_modal_icon": "Icon",
"edit_game_modal_select_icon": "Select icon",
"edit_game_modal_icon_preview": "Icon preview",
"edit_game_modal_logo": "Logo",
"edit_game_modal_select_logo": "Select logo",
"edit_game_modal_logo_preview": "Logo preview",
"edit_game_modal_hero": "Library Hero",
"edit_game_modal_select_hero": "Select library hero image",
"edit_game_modal_hero_preview": "Library hero image preview",
"edit_game_modal_cancel": "Cancel",
"edit_game_modal_update": "Update",
"edit_game_modal_updating": "Updating...",
"edit_game_modal_fill_required": "Please fill in all required fields",
"edit_game_modal_success": "Assets updated successfully",
"edit_game_modal_failed": "Failed to update assets",
"edit_game_modal_image_filter": "Image",
"edit_game_modal_icon_resolution": "Recommended resolution: 256x256px",
"edit_game_modal_logo_resolution": "Recommended resolution: 640x360px",
"edit_game_modal_hero_resolution": "Recommended resolution: 1920x620px",
"edit_game_modal_assets": "Assets",
"edit_game_modal_drop_icon_image_here": "Drop icon image here",
"edit_game_modal_drop_logo_image_here": "Drop logo image here",
"edit_game_modal_drop_hero_image_here": "Drop hero image here",
"edit_game_modal_drop_to_replace_icon": "Drop to replace icon",
"edit_game_modal_drop_to_replace_logo": "Drop to replace logo",
"edit_game_modal_drop_to_replace_hero": "Drop to replace hero"
}, },
"header": { "header": {
"search": "Search games", "search": "Search games",
@@ -279,7 +231,6 @@
"backup_unfrozen": "Backup unpinned", "backup_unfrozen": "Backup unpinned",
"backup_freeze_failed": "Failed to freeze backup", "backup_freeze_failed": "Failed to freeze backup",
"backup_freeze_failed_description": "You must leave at least one free slot for automatic backups", "backup_freeze_failed_description": "You must leave at least one free slot for automatic backups",
"edit_game_modal_button": "Customize game assets",
"game_details": "Game Details", "game_details": "Game Details",
"currency_symbol": "$", "currency_symbol": "$",
"currency_country": "us", "currency_country": "us",
@@ -290,11 +241,10 @@
"keyshop_price": "Keyshop price", "keyshop_price": "Keyshop price",
"historical_retail": "Historical retail", "historical_retail": "Historical retail",
"historical_keyshop": "Historical keyshop", "historical_keyshop": "Historical keyshop",
"supported_languages": "Supported languages",
"language": "Language", "language": "Language",
"caption": "Caption", "caption": "Caption",
"audio": "Audio", "audio": "Audio"
"filter_by_source": "Filter by source",
"no_repacks_found": "No sources found for this game"
}, },
"activation": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",
@@ -332,6 +282,7 @@
"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",
"extract": "Extract files", "extract": "Extract files",
"extracting": "Extracting files…" "extracting": "Extracting files…"
}, },
@@ -340,6 +291,7 @@
"change": "Update", "change": "Update",
"notifications": "Notifications", "notifications": "Notifications",
"enable_download_notifications": "When a download is complete", "enable_download_notifications": "When a download is complete",
"gg_deals_api_key_description": "gg deals api key. Used to show the lowest price. (https://gg.deals/api/)",
"enable_repack_list_notifications": "When a new repack is added", "enable_repack_list_notifications": "When a new repack is added",
"real_debrid_api_token_label": "Real-Debrid API token", "real_debrid_api_token_label": "Real-Debrid API token",
"quit_app_instead_hiding": "Don't hide Hydra when closing", "quit_app_instead_hiding": "Don't hide Hydra when closing",
@@ -441,6 +393,17 @@
"create_real_debrid_account": "Click here if you don't have a Real-Debrid account yet", "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", "create_torbox_account": "Click here if you don't have a TorBox account yet",
"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",
"name_min_length": "Theme name must be at least 3 characters long", "name_min_length": "Theme name must be at least 3 characters long",
"import_theme": "Import theme", "import_theme": "Import theme",
"import_theme_description": "You will import {{theme}} from the theme store", "import_theme_description": "You will import {{theme}} from the theme store",
@@ -514,8 +477,6 @@
"user_profile": { "user_profile": {
"amount_hours": "{{amount}} hours", "amount_hours": "{{amount}} hours",
"amount_minutes": "{{amount}} minutes", "amount_minutes": "{{amount}} minutes",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"last_time_played": "Last played {{period}}", "last_time_played": "Last played {{period}}",
"activity": "Recent Activity", "activity": "Recent Activity",
"library": "Library", "library": "Library",

View File

@@ -1,6 +1,7 @@
{ {
"language_name": "Română", "language_name": "Română",
"home": { "home": {
"featured": "Recomandate",
"surprise_me": "Surprinde-mă", "surprise_me": "Surprinde-mă",
"no_results": "Niciun rezultat găsit" "no_results": "Niciun rezultat găsit"
}, },
@@ -18,6 +19,7 @@
}, },
"header": { "header": {
"search": "Caută jocuri", "search": "Caută jocuri",
"home": "Acasă", "home": "Acasă",
"catalogue": "Catalog", "catalogue": "Catalog",
"downloads": "Descărcări", "downloads": "Descărcări",
@@ -30,7 +32,10 @@
"downloading": "Se descarcă {{title}}... ({{percentage}} complet) - Concluzie {{eta}} - {{speed}}", "downloading": "Se descarcă {{title}}... ({{percentage}} complet) - Concluzie {{eta}} - {{speed}}",
"calculating_eta": "Se descarcă {{title}}... ({{percentage}} complet) - Calculare timp rămas..." "calculating_eta": "Se descarcă {{title}}... ({{percentage}} complet) - Calculare timp rămas..."
}, },
"catalogue": {}, "catalogue": {
"next_page": "Pagina următoare",
"previous_page": "Pagina anterioară"
},
"game_details": { "game_details": {
"open_download_options": "Deschide opțiunile de descărcare", "open_download_options": "Deschide opțiunile de descărcare",
"download_options_zero": "Nicio opțiune de descărcare", "download_options_zero": "Nicio opțiune de descărcare",
@@ -135,7 +140,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ă",

View File

@@ -14,9 +14,6 @@ import "./catalogue/get-developers";
import "./hardware/get-disk-free-space"; import "./hardware/get-disk-free-space";
import "./hardware/check-folder-write-permission"; import "./hardware/check-folder-write-permission";
import "./library/add-game-to-library"; import "./library/add-game-to-library";
import "./library/add-custom-game-to-library";
import "./library/update-custom-game";
import "./library/update-game-custom-assets";
import "./library/add-game-to-favorites"; import "./library/add-game-to-favorites";
import "./library/remove-game-from-favorites"; import "./library/remove-game-from-favorites";
import "./library/toggle-game-pin"; import "./library/toggle-game-pin";
@@ -40,9 +37,7 @@ import "./library/reset-game-achievements";
import "./library/change-game-playtime"; import "./library/change-game-playtime";
import "./library/toggle-automatic-cloud-sync"; import "./library/toggle-automatic-cloud-sync";
import "./library/get-default-wine-prefix-selection-path"; import "./library/get-default-wine-prefix-selection-path";
import "./library/cleanup-unused-assets";
import "./library/create-steam-shortcut"; import "./library/create-steam-shortcut";
import "./library/copy-custom-game-asset";
import "./misc/open-checkout"; import "./misc/open-checkout";
import "./misc/open-external"; import "./misc/open-external";
import "./misc/show-open-dialog"; import "./misc/show-open-dialog";
@@ -51,10 +46,9 @@ import "./misc/show-item-in-folder";
import "./misc/get-badges"; import "./misc/get-badges";
import "./misc/install-common-redist"; import "./misc/install-common-redist";
import "./misc/can-install-common-redist"; import "./misc/can-install-common-redist";
import "./misc/save-temp-file";
import "./misc/delete-temp-file";
import "./torrenting/cancel-game-download"; import "./torrenting/cancel-game-download";
import "./torrenting/pause-game-download"; import "./torrenting/pause-game-download";
import "./user-preferences/authenticate-all-debrid";
import "./torrenting/resume-game-download"; import "./torrenting/resume-game-download";
import "./torrenting/start-game-download"; import "./torrenting/start-game-download";
import "./torrenting/pause-game-seed"; import "./torrenting/pause-game-seed";

View File

@@ -0,0 +1,17 @@
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);

View File

@@ -8,6 +8,7 @@ import {
CommonRedistManager, CommonRedistManager,
TorBoxClient, TorBoxClient,
RealDebridClient, RealDebridClient,
AllDebridClient,
Aria2, Aria2,
DownloadManager, DownloadManager,
HydraApi, HydraApi,
@@ -37,6 +38,10 @@ export const loadState = async () => {
RealDebridClient.authorize(userPreferences.realDebridApiToken); RealDebridClient.authorize(userPreferences.realDebridApiToken);
} }
if (userPreferences?.allDebridApiKey) {
AllDebridClient.authorize(userPreferences.allDebridApiKey);
}
if (userPreferences?.torBoxApiToken) { if (userPreferences?.torBoxApiToken) {
TorBoxClient.authorize(userPreferences.torBoxApiToken); TorBoxClient.authorize(userPreferences.torBoxApiToken);
} }

View File

@@ -0,0 +1,315 @@
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
);
console.log("[AllDebrid] FINAL 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
}
}

View File

@@ -17,6 +17,7 @@ import {
} from "./types"; } from "./types";
import { calculateETA, getDirSize } from "./helpers"; import { calculateETA, getDirSize } from "./helpers";
import { RealDebridClient } from "./real-debrid"; import { RealDebridClient } from "./real-debrid";
import { AllDebridClient } from "./all-debrid";
import path from "path"; import path from "path";
import { logger } from "../logger"; import { logger } from "../logger";
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
@@ -40,6 +41,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,
@@ -377,6 +379,27 @@ export class DownloadManager {
allow_multiple_connections: true, allow_multiple_connections: true,
}; };
} }
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.TorBox: { case Downloader.TorBox: {
const { name, url } = await TorBoxClient.getDownloadInfo(download.uri); const { name, url } = await TorBoxClient.getDownloadInfo(download.uri);

View File

@@ -17,6 +17,14 @@ export const calculateETA = (
}; };
export const getDirSize = async (dir: string): Promise<number> => { export const getDirSize = async (dir: string): Promise<number> => {
try {
const stat = await fs.promises.stat(dir);
// If it's a file, return its size directly
if (!stat.isDirectory()) {
return stat.size;
}
const getItemSize = async (filePath: string): Promise<number> => { const getItemSize = async (filePath: string): Promise<number> => {
const stat = await fs.promises.stat(filePath); const stat = await fs.promises.stat(filePath);
@@ -27,7 +35,6 @@ export const getDirSize = async (dir: string): Promise<number> => {
return stat.size; 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));

View File

@@ -1,3 +1,4 @@
export * from "./download-manager"; export * from "./download-manager";
export * from "./real-debrid"; export * from "./real-debrid";
export * from "./all-debrid";
export * from "./torbox"; export * from "./torbox";

View File

@@ -11,9 +11,13 @@ import { app, dialog } from "electron";
import { db, levelKeys } from "@main/level"; import { db, levelKeys } from "@main/level";
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>> = {

View File

@@ -102,6 +102,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),
@@ -128,52 +130,6 @@ contextBridge.exposeInMainWorld("electron", {
), ),
addGameToLibrary: (shop: GameShop, objectId: string, title: string) => addGameToLibrary: (shop: GameShop, objectId: string, title: string) =>
ipcRenderer.invoke("addGameToLibrary", shop, objectId, title), ipcRenderer.invoke("addGameToLibrary", shop, objectId, title),
addCustomGameToLibrary: (
title: string,
executablePath: string,
iconUrl?: string,
logoImageUrl?: string,
libraryHeroImageUrl?: string
) =>
ipcRenderer.invoke(
"addCustomGameToLibrary",
title,
executablePath,
iconUrl,
logoImageUrl,
libraryHeroImageUrl
),
copyCustomGameAsset: (
sourcePath: string,
assetType: "icon" | "logo" | "hero"
) => ipcRenderer.invoke("copyCustomGameAsset", sourcePath, assetType),
saveTempFile: (fileName: string, fileData: Uint8Array) =>
ipcRenderer.invoke("saveTempFile", fileName, fileData),
deleteTempFile: (filePath: string) =>
ipcRenderer.invoke("deleteTempFile", filePath),
cleanupUnusedAssets: () => ipcRenderer.invoke("cleanupUnusedAssets"),
updateCustomGame: (params: {
shop: GameShop;
objectId: string;
title: string;
iconUrl?: string;
logoImageUrl?: string;
libraryHeroImageUrl?: string;
originalIconPath?: string;
originalLogoPath?: string;
originalHeroPath?: string;
}) => ipcRenderer.invoke("updateCustomGame", params),
updateGameCustomAssets: (params: {
shop: GameShop;
objectId: string;
title: string;
customIconUrl?: string | null;
customLogoImageUrl?: string | null;
customHeroImageUrl?: string | null;
customOriginalIconPath?: string | null;
customOriginalLogoPath?: string | null;
customOriginalHeroPath?: string | null;
}) => ipcRenderer.invoke("updateGameCustomAssets", params),
createGameShortcut: ( createGameShortcut: (
shop: GameShop, shop: GameShop,
objectId: string, objectId: string,

View File

@@ -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",
[Downloader.Hydra]: "Nimbus", [Downloader.Hydra]: "Nimbus",
}; };

View File

@@ -28,6 +28,7 @@ import type {
LibraryGame, LibraryGame,
GameRunning, GameRunning,
TorBoxUser, TorBoxUser,
AllDebridUser,
Theme, Theme,
Badge, Badge,
Auth, Auth,
@@ -112,43 +113,6 @@ declare global {
objectId: string, objectId: string,
title: string title: string
) => Promise<void>; ) => Promise<void>;
addCustomGameToLibrary: (
title: string,
executablePath: string,
iconUrl?: string,
logoImageUrl?: string,
libraryHeroImageUrl?: string
) => Promise<Game>;
updateCustomGame: (params: {
shop: GameShop;
objectId: string;
title: string;
iconUrl?: string;
logoImageUrl?: string;
libraryHeroImageUrl?: string;
originalIconPath?: string;
originalLogoPath?: string;
originalHeroPath?: string;
}) => Promise<Game>;
copyCustomGameAsset: (
sourcePath: string,
assetType: "icon" | "logo" | "hero"
) => Promise<string>;
cleanupUnusedAssets: () => Promise<{
deletedCount: number;
errors: string[];
}>;
updateGameCustomAssets: (params: {
shop: GameShop;
objectId: string;
title: string;
customIconUrl?: string | null;
customLogoImageUrl?: string | null;
customHeroImageUrl?: string | null;
customOriginalIconPath?: string | null;
customOriginalLogoPath?: string | null;
customOriginalHeroPath?: string | null;
}) => Promise<Game>;
createGameShortcut: ( createGameShortcut: (
shop: GameShop, shop: GameShop,
objectId: string, objectId: string,
@@ -212,6 +176,9 @@ declare global {
) => Promise<void>; ) => Promise<void>;
/* User preferences */ /* User preferences */
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>;
getUserPreferences: () => Promise<UserPreferences | null>; getUserPreferences: () => Promise<UserPreferences | null>;
updateUserPreferences: ( updateUserPreferences: (
@@ -310,8 +277,6 @@ declare global {
onCommonRedistProgress: ( onCommonRedistProgress: (
cb: (value: { log: string; complete: boolean }) => void cb: (value: { log: string; complete: boolean }) => void
) => () => Electron.IpcRenderer; ) => () => Electron.IpcRenderer;
saveTempFile: (fileName: string, fileData: Uint8Array) => Promise<string>;
deleteTempFile: (filePath: string) => Promise<void>;
platform: NodeJS.Platform; platform: NodeJS.Platform;
/* Auto update */ /* Auto update */

View File

@@ -114,6 +114,15 @@ export function DownloadGroup({
return <p>{t("deleting")}</p>; return <p>{t("deleting")}</p>;
} }
if (download.downloader === Downloader.AllDebrid) {
return (
<>
<p>{progress}</p>
<p>{t("alldebrid_size_not_supported")}</p>
</>
);
}
if (isGameDownloading) { if (isGameDownloading) {
if (lastPacket?.isDownloadingMetadata) { if (lastPacket?.isDownloadingMetadata) {
return <p>{t("downloading_metadata")}</p>; return <p>{t("downloading_metadata")}</p>;
@@ -181,6 +190,15 @@ export function DownloadGroup({
} }
if (download.status === "active") { if (download.status === "active") {
if ((download.downloader as unknown as string) === "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>
@@ -275,7 +293,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 [
{ {

View File

@@ -229,7 +229,7 @@ export function HeroPanelActions() {
{game.favorite ? <HeartFillIcon /> : <HeartIcon />} {game.favorite ? <HeartFillIcon /> : <HeartIcon />}
</Button> </Button>
{userDetails && game.shop !== "custom" && ( {userDetails && (
<Button <Button
onClick={toggleGamePinned} onClick={toggleGamePinned}
theme="outline" theme="outline"

View File

@@ -117,6 +117,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;
if (downloader === Downloader.Hydra) if (downloader === Downloader.Hydra)
return isFeatureEnabled(Feature.Nimbus); return isFeatureEnabled(Feature.Nimbus);
return true; return true;
@@ -131,6 +133,7 @@ export function DownloadSettingsModal({
downloaders, downloaders,
userPreferences?.realDebridApiToken, userPreferences?.realDebridApiToken,
userPreferences?.torBoxApiToken, userPreferences?.torBoxApiToken,
userPreferences?.allDebridApiKey,
]); ]);
const handleChooseDownloadsPath = async () => { const handleChooseDownloadsPath = async () => {
@@ -191,6 +194,8 @@ export function DownloadSettingsModal({
const shouldDisableButton = const shouldDisableButton =
(downloader === Downloader.RealDebrid && (downloader === Downloader.RealDebrid &&
!userPreferences?.realDebridApiToken) || !userPreferences?.realDebridApiToken) ||
(downloader === Downloader.AllDebrid &&
!userPreferences?.allDebridApiKey) ||
(downloader === Downloader.TorBox && (downloader === Downloader.TorBox &&
!userPreferences?.torBoxApiToken) || !userPreferences?.torBoxApiToken) ||
(downloader === Downloader.Hydra && (downloader === Downloader.Hydra &&

View File

@@ -0,0 +1,12 @@
.settings-all-debrid {
&__form {
display: flex;
flex-direction: column;
gap: 1rem;
}
&__description {
margin: 0;
color: var(--text-secondary);
}
}

View 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>
);
}

View File

@@ -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 { SettingsDownloadSources } from "./settings-download-sources"; import { SettingsDownloadSources } from "./settings-download-sources";
@@ -42,6 +43,7 @@ export default function Settings() {
] ]
: []), : []),
{ tabLabel: "Real-Debrid", contentTitle: "Real-Debrid" }, { tabLabel: "Real-Debrid", contentTitle: "Real-Debrid" },
{ tabLabel: "All-Debrid", contentTitle: "All-Debrid" },
]; ];
if (userDetails) if (userDetails)
@@ -81,6 +83,10 @@ export default function Settings() {
return <SettingsRealDebrid />; return <SettingsRealDebrid />;
} }
if (currentCategoryIndex === 6) {
return <SettingsAllDebrid />;
}
return <SettingsAccount />; return <SettingsAccount />;
}; };

View File

@@ -1,5 +1,6 @@
export enum Downloader { export enum Downloader {
RealDebrid, RealDebrid,
AllDebrid,
Torrent, Torrent,
Gofile, Gofile,
PixelDrain, PixelDrain,
@@ -55,6 +56,7 @@ export enum AuthPage {
export enum DownloadError { export enum DownloadError {
NotCachedOnRealDebrid = "download_error_not_cached_on_real_debrid", NotCachedOnRealDebrid = "download_error_not_cached_on_real_debrid",
NotCachedInAllDebrid = "download_error_not_cached_in_alldebrid",
NotCachedOnTorBox = "download_error_not_cached_on_torbox", NotCachedOnTorBox = "download_error_not_cached_on_torbox",
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",

View File

@@ -123,6 +123,7 @@ export const getDownloadersForUri = (uri: string) => {
Downloader.Hydra, Downloader.Hydra,
Downloader.TorBox, Downloader.TorBox,
Downloader.RealDebrid, Downloader.RealDebrid,
Downloader.AllDebrid,
]; ];
} }

View File

@@ -175,3 +175,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;
}

View File

@@ -33,17 +33,6 @@ export interface User {
export interface Game { export interface Game {
title: string; title: string;
iconUrl: string | null; iconUrl: string | null;
libraryHeroImageUrl: string | null;
logoImageUrl: string | null;
customIconUrl?: string | null;
customLogoImageUrl?: string | null;
customHeroImageUrl?: string | null;
originalIconPath?: string | null;
originalLogoPath?: string | null;
originalHeroPath?: string | null;
customOriginalIconPath?: string | null;
customOriginalLogoPath?: string | null;
customOriginalHeroPath?: string | null;
playTimeInMilliseconds: number; playTimeInMilliseconds: number;
unsyncedDeltaPlayTimeInMilliseconds?: number; unsyncedDeltaPlayTimeInMilliseconds?: number;
lastTimePlayed: Date | null; lastTimePlayed: Date | null;
@@ -95,9 +84,11 @@ export type AchievementCustomNotificationPosition =
export interface UserPreferences { export interface UserPreferences {
downloadsPath?: string | null; downloadsPath?: string | null;
ggDealsApiKey?: string | null;
language?: string; language?: string;
realDebridApiToken?: string | null; realDebridApiToken?: string | null;
torBoxApiToken?: string | null; torBoxApiToken?: string | null;
allDebridApiKey?: string | null;
preferQuitInsteadOfHiding?: boolean; preferQuitInsteadOfHiding?: boolean;
runAtStartup?: boolean; runAtStartup?: boolean;
startMinimized?: boolean; startMinimized?: boolean;

View File

@@ -8,6 +8,7 @@
"src/locales/index.ts", "src/locales/index.ts",
"src/shared/**/*", "src/shared/**/*",
"src/stories/**/*", "src/stories/**/*",
"src/types/**/*",
".storybook/**/*" ".storybook/**/*"
], ],
"compilerOptions": { "compilerOptions": {