diff --git a/.gitignore b/.gitignore index 1cd10467..624a4076 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .vscode node_modules -aria2* +aria2/ fastlist.exe __pycache__ dist diff --git a/src/locales/ar/translation.json b/src/locales/ar/translation.json index 3ea46b4f..5e2d05ef 100644 --- a/src/locales/ar/translation.json +++ b/src/locales/ar/translation.json @@ -134,7 +134,7 @@ "delete_modal_title": "هل أنت متأكد؟", "delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهاز الكمبيوتر الخاص بك", "install": "تثبيت", - "real_debrid": "Real Debrid", + "real_debrid": "Real-Debrid", "torrent": "تورنت" }, "settings": { @@ -145,13 +145,13 @@ "enable_repack_list_notifications": "عند إضافة حزمة جديدة", "telemetry": "القياس عن بعد", "telemetry_description": "تفعيل إحصائيات الاستخدام مجهولة المصدر", - "real_debrid_api_token_label": "رمز واجهة برمجة التطبيقات (API) لـReal Debrid ", + "real_debrid_api_token_label": "رمز واجهة برمجة التطبيقات (API) لـReal-Debrid ", "quit_app_instead_hiding": "إنهاء هايدرا بدلاً من التصغير الى شريط الحالة", "launch_with_system": "تشغيل هايدرا عند بدء تشغيل النظام", "general": "عام", "behavior": "السلوك", - "enable_real_debrid": "تفعيل Real Debrid ", - "real_debrid": "Real Debrid", + "enable_real_debrid": "تفعيل Real-Debrid ", + "real_debrid": "Real-Debrid", "real_debrid_api_token_hint": "يمكنك الحصول على مفتاح API الخاص بك هنا.", "save_changes": "حفظ التغييرات" }, diff --git a/src/locales/da/translation.json b/src/locales/da/translation.json index dca04160..4a184330 100644 --- a/src/locales/da/translation.json +++ b/src/locales/da/translation.json @@ -128,7 +128,7 @@ "delete_modal_title": "Er du sikker?", "delete_modal_description": "Dette vil fjerne alle installations filerne fra din computer", "install": "Installér", - "real_debrid": "Real Debrid", + "real_debrid": "Real-Debrid", "torrent": "Torrent" }, "settings": { @@ -139,13 +139,13 @@ "enable_repack_list_notifications": "Når en ny repack bliver tilføjet", "telemetry": "Telemetri", "telemetry_description": "Slå anonymt brugs statistik til", - "real_debrid_api_token_description": "Real Debrid API token", + "real_debrid_api_token_description": "Real-Debrid API token", "quit_app_instead_hiding": "Afslut Hydra instedet for at minimere til processlinjen", "launch_with_system": "Åben Hydra ved start af systemet", "general": "Generelt", "behavior": "Opførsel", - "enable_real_debrid": "Slå Real Debrid til", - "real_debrid": "Real Debrid", + "enable_real_debrid": "Slå Real-Debrid til", + "real_debrid": "Real-Debrid", "real_debrid_api_token_hint": "Du kan få din API nøgle <0>her.", "save_changes": "Gem ændringer" }, diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 417df3b7..6185fbf8 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -133,7 +133,7 @@ "delete_modal_title": "Are you sure?", "delete_modal_description": "This will remove all the installation files from your computer", "install": "Install", - "real_debrid": "Real Debrid", + "real_debrid": "Real-Debrid", "torrent": "Torrent" }, "settings": { @@ -144,14 +144,15 @@ "enable_repack_list_notifications": "When a new repack is added", "telemetry": "Telemetry", "telemetry_description": "Enable anonymous usage statistics", - "real_debrid_api_token_label": "Real Debrid API token", + "real_debrid_api_token_label": "Real-Debrid API token", "quit_app_instead_hiding": "Quit Hydra instead of minimizing to tray", "launch_with_system": "Launch Hydra on system start-up", "general": "General", "behavior": "Behavior", - "enable_real_debrid": "Enable Real Debrid", - "real_debrid": "Real Debrid", - "real_debrid_api_token_hint": "You can get your API key <0>here.", + "enable_real_debrid": "Enable Real-Debrid", + "real_debrid": "Real-Debrid", + "real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to download files instantly and at the best of your Internet speed.", + "real_debrid_api_token_hint": "You can get your API token <0>here.", "save_changes": "Save changes" }, "notifications": { diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 822e50ea..945f2121 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -134,7 +134,7 @@ "delete_modal_title": "¿Estás seguro?", "delete_modal_description": "Esto eliminará todos los archivos de instalación de tu computadora.", "install": "Instalar", - "real_debrid": "Real Debrid", + "real_debrid": "Real-Debrid", "torrent": "Torrent" }, "settings": { @@ -145,13 +145,13 @@ "enable_repack_list_notifications": "Cuando se añade un repack nuevo", "telemetry": "Telemetría", "telemetry_description": "Habilitar recopilación de datos de manera anónima", - "real_debrid_api_token_label": "Token API de Real Debrid", + "real_debrid_api_token_label": "Token API de Real-Debrid", "quit_app_instead_hiding": "Salir de Hydra en vez de minimizar en la bandeja del sistema", "launch_with_system": "Iniciar Hydra al inicio del sistema", "general": "General", "behavior": "Otros", - "enable_real_debrid": "Activar Real Debrid", - "real_debrid": "Real Debrid", + "enable_real_debrid": "Activar Real-Debrid", + "real_debrid": "Real-Debrid", "real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí.", "save_changes": "Guardar cambios" }, diff --git a/src/locales/fa/translation.json b/src/locales/fa/translation.json index aa929945..4c8e3e7d 100644 --- a/src/locales/fa/translation.json +++ b/src/locales/fa/translation.json @@ -128,7 +128,7 @@ "delete_modal_title": "مطمئنی؟", "delete_modal_description": "این کار تمام فایل‌های اینستالر را از کامپیوتر شما حذف می‌کند", "install": "نصف", - "real_debrid": "Real Debrid", + "real_debrid": "Real-Debrid", "torrent": "تورنت" }, "settings": { @@ -139,13 +139,13 @@ "enable_repack_list_notifications": "زمانی که یک ریپک جدید اضافه شد", "telemetry": "تلمتری", "telemetry_description": "فعال کردن آمارگیری استفاده ناشناس", - "real_debrid_api_token_description": "توکن Real Debrid", + "real_debrid_api_token_description": "توکن Real-Debrid", "quit_app_instead_hiding": "به جای کوچک کردن، از هایدرا خارج شو", "launch_with_system": "زمانی که سیستم روشن می‌شود، هایدرا را باز کن", "general": "کلی", "behavior": "رفتار", - "enable_real_debrid": "فعال‌سازی Real Debrid", - "real_debrid": "Real Debrid", + "enable_real_debrid": "فعال‌سازی Real-Debrid", + "real_debrid": "Real-Debrid", "real_debrid_api_token_hint": "کلید API خود را از <ب0>اینجا بگیرید.", "save_changes": "ذخیره تغییرات" }, diff --git a/src/locales/ko/translation.json b/src/locales/ko/translation.json index 94a945d1..11f8afc2 100644 --- a/src/locales/ko/translation.json +++ b/src/locales/ko/translation.json @@ -128,7 +128,7 @@ "delete_modal_title": "정말로 하시겠습니까?", "delete_modal_description": "이 기기의 모든 설치 파일들이 제거될 것입니다", "install": "설치", - "real_debrid": "Real Debrid", + "real_debrid": "Real-Debrid", "torrent": "Torrent" }, "settings": { @@ -139,13 +139,13 @@ "enable_repack_list_notifications": "새 리팩이 추가되었을 때", "telemetry": "자동 데이터 수집", "telemetry_description": "익명 사용 통계를 활성화", - "real_debrid_api_token_description": "Real Debrid API 토큰", + "real_debrid_api_token_description": "Real-Debrid API 토큰", "quit_app_instead_hiding": "작업 표시줄 트레이로 최소화하는 대신 Hydra를 종료", "launch_with_system": "컴퓨터가 시작되었을 때 Hydra 실행", "general": "일반", "behavior": "행동", - "enable_real_debrid": "Real Debrid 활성화", - "real_debrid": "Real Debrid", + "enable_real_debrid": "Real-Debrid 활성화", + "real_debrid": "Real-Debrid", "real_debrid_api_token_hint": "API 키를 <0>이곳에서 얻으세요.", "save_changes": "변경 사항 저장" }, diff --git a/src/locales/nl/translation.json b/src/locales/nl/translation.json index 4be69007..066f8813 100644 --- a/src/locales/nl/translation.json +++ b/src/locales/nl/translation.json @@ -128,7 +128,7 @@ "delete_modal_title": "Weet je het zeker?", "delete_modal_description": "Hiermee worden alle installatiebestanden van uw computer verwijderd", "install": "Installeren", - "real_debrid": "Real Debrid", + "real_debrid": "Real-Debrid", "torrent": "Torrent" }, "settings": { @@ -139,13 +139,13 @@ "enable_repack_list_notifications": "Wanneer een nieuwe herverpakking wordt toegevoegd", "telemetry": "Telemetrie", "telemetry_description": "Schakel anonieme gebruiksstatistieken in", - "real_debrid_api_token_label": "Real Debrid API token", + "real_debrid_api_token_label": "Real-Debrid API token", "quit_app_instead_hiding": "Sluit Hydra af in plaats van te minimaliseren naar de lade", "launch_with_system": "Start Hydra bij het opstarten van het systeem", "general": "Algemeen", "behavior": "Gedrag", - "enable_real_debrid": "Enable Real Debrid", - "real_debrid": "Real Debrid", + "enable_real_debrid": "Enable Real-Debrid", + "real_debrid": "Real-Debrid", "real_debrid_api_token_hint": "U kunt uw API-sleutel <0>hier verkrijgen.", "save_changes": "Wijzigingen opslaan" }, diff --git a/src/locales/pl/translation.json b/src/locales/pl/translation.json index d629d1f3..4ecff6d3 100644 --- a/src/locales/pl/translation.json +++ b/src/locales/pl/translation.json @@ -134,7 +134,7 @@ "delete_modal_title": "Czy na pewno?", "delete_modal_description": "Spowoduje to usunięcie wszystkich plików instalacyjnych z komputera", "install": "Instaluj", - "real_debrid": "Real Debrid", + "real_debrid": "Real-Debrid", "torrent": "Torrent" }, "settings": { @@ -145,13 +145,13 @@ "enable_repack_list_notifications": "Gdy dodawany jest nowy repack", "telemetry": "Telemetria", "telemetry_description": "Włącz anonimowe statystyki użycia", - "real_debrid_api_token_label": "Real Debrid API token", + "real_debrid_api_token_label": "Real-Debrid API token", "quit_app_instead_hiding": "Zamknij Hydr zamiast minimalizować do zasobnika", "launch_with_system": "Uruchom Hydra przy starcie systemu", "general": "Ogólne", "behavior": "Zachowania", - "enable_real_debrid": "Włącz Real Debrid", - "real_debrid": "Real Debrid", + "enable_real_debrid": "Włącz Real-Debrid", + "real_debrid": "Real-Debrid", "real_debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj.", "save_changes": "Zapisz zmiany" }, diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index d5f92a9f..834f9bf3 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -131,7 +131,7 @@ "deleting": "Excluindo instalador…", "install": "Instalar", "torrent": "Torrent", - "real_debrid": "Real Debrid" + "real_debrid": "Real-Debrid" }, "settings": { "downloads_path": "Diretório dos downloads", @@ -141,13 +141,13 @@ "enable_repack_list_notifications": "Quando a lista de repacks for atualizada", "telemetry": "Telemetria", "telemetry_description": "Habilitar estatísticas de uso anônimas", - "real_debrid_api_token_label": "Token de API do Real Debrid", + "real_debrid_api_token_label": "Token de API do Real-Debrid", "quit_app_instead_hiding": "Fechar o aplicativo em vez de minimizá-lo", "launch_with_system": "Iniciar aplicativo na inicialização do sistema", "general": "Geral", "behavior": "Comportamento", - "enable_real_debrid": "Habilitar Real Debrid", - "real_debrid": "Real Debrid", + "enable_real_debrid": "Habilitar Real-Debrid", + "real_debrid": "Real-Debrid", "real_debrid_api_token_hint": "Você pode obter sua chave de API <0>aqui.", "save_changes": "Salvar mudanças" }, diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index f7a80771..e8ed817e 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -134,7 +134,7 @@ "delete_modal_title": "Вы уверены?", "delete_modal_description": "Это удалит все установщики с вашего компьютера", "install": "Установить", - "real_debrid": "Real Debrid", + "real_debrid": "Real-Debrid", "torrent": "Torrent" }, "settings": { @@ -145,13 +145,13 @@ "enable_repack_list_notifications": "При добавлении нового репака", "telemetry": "Телеметрия", "telemetry_description": "Отправлять анонимную статистику использования", - "real_debrid_api_token_label": "Real Debrid API-токен", + "real_debrid_api_token_label": "Real-Debrid API-токен", "quit_app_instead_hiding": "Закрывать Hydra вместо того, чтобы сворачивать его в трей", "launch_with_system": "Запуск Hydra вместе с системой", "general": "Основные", "behavior": "Поведение", - "enable_real_debrid": "Включить Real Debrid", - "real_debrid": "Real Debrid", + "enable_real_debrid": "Включить Real-Debrid", + "real_debrid": "Real-Debrid", "real_debrid_api_token_hint": "API ключ можно получить <0>здесь.", "save_changes": "Сохранить изменения" }, diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 134e83a1..18facd1a 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -132,7 +132,7 @@ "delete_modal_title": "您确定吗?", "delete_modal_description": "这将从您的电脑上移除所有的安装文件", "install": "安装", - "real_debrid": "Real Debrid", + "real_debrid": "Real-Debrid", "torrent": "种子" }, "settings": { @@ -143,13 +143,13 @@ "enable_repack_list_notifications": "添加新重打包时", "telemetry": "遥测", "telemetry_description": "启用匿名使用统计", - "real_debrid_api_token_description": "Real Debrid API密钥", + "real_debrid_api_token_description": "Real-Debrid API密钥", "behavior": "行为", "general": "常规", "quit_app_instead_hiding": "关闭应用程序而不是最小化到托盘", "launch_with_system": "随系统启动时运行应用程序", - "enable_real_debrid": "启用 Real Debrid", - "real_debrid": "Real Debrid", + "enable_real_debrid": "启用 Real-Debrid", + "real_debrid": "Real-Debrid", "real_debrid_api_token_hint": "您可以从<0>这里获取API密钥.", "save_changes": "保存更改" }, diff --git a/src/main/events/torrenting/resume-game-download.ts b/src/main/events/torrenting/resume-game-download.ts index b39ee8f3..f8007206 100644 --- a/src/main/events/torrenting/resume-game-download.ts +++ b/src/main/events/torrenting/resume-game-download.ts @@ -29,7 +29,7 @@ const resumeGameDownload = async ( .getRepository(Game) .update({ status: "active", progress: Not(1) }, { status: "paused" }); - await DownloadManager.resumeDownload(gameId); + await DownloadManager.resumeDownload(game); await transactionalEntityManager .getRepository(Game) diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 02b36f4a..85a4d483 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -19,6 +19,7 @@ const startGameDownload = async ( where: { objectID, }, + relations: { repack: true }, }), repackRepository.findOne({ where: { @@ -50,9 +51,7 @@ const startGameDownload = async ( } ); - await DownloadManager.startDownload(game.id); - - return { ...game, stauts: "active" }; + return DownloadManager.startDownload(game); } else { const steamGame = stateManager .getValue("steamGames") @@ -62,8 +61,8 @@ const startGameDownload = async ( ? getSteamAppAsset("icon", objectID, steamGame.clientIcon) : null; - const createdGame = await gameRepository - .save({ + await gameRepository + .insert({ title, iconUrl, objectID, @@ -83,9 +82,14 @@ const startGameDownload = async ( return result; }); - await DownloadManager.startDownload(createdGame.id); + const createdGame = await gameRepository.findOne({ + where: { + objectID, + }, + relations: { repack: true }, + }); - return createdGame; + return DownloadManager.startDownload(createdGame!); } }; diff --git a/src/main/events/user-preferences/update-user-preferences.ts b/src/main/events/user-preferences/update-user-preferences.ts index a62331f4..66d56d4a 100644 --- a/src/main/events/user-preferences/update-user-preferences.ts +++ b/src/main/events/user-preferences/update-user-preferences.ts @@ -2,23 +2,17 @@ import { userPreferencesRepository } from "@main/repository"; import { registerEvent } from "../register-event"; import type { UserPreferences } from "@types"; -import { RealDebridClient } from "@main/services/real-debrid"; const updateUserPreferences = async ( _event: Electron.IpcMainInvokeEvent, preferences: Partial -) => { - if (preferences.realDebridApiToken) { - RealDebridClient.authorize(preferences.realDebridApiToken); - } - - await userPreferencesRepository.upsert( +) => + userPreferencesRepository.upsert( { id: 1, ...preferences, }, ["id"] ); -}; registerEvent("updateUserPreferences", updateUserPreferences); diff --git a/src/main/main.ts b/src/main/main.ts index 63162bbf..501f03fa 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -97,7 +97,7 @@ const loadState = async (userPreferences: UserPreferences | null) => { relations: { repack: true }, }); - if (game) DownloadManager.startDownload(game.id); + if (game) DownloadManager.startDownload(game); }; userPreferencesRepository diff --git a/src/main/services/aria2.ts b/src/main/services/aria2.ts new file mode 100644 index 00000000..df624cd1 --- /dev/null +++ b/src/main/services/aria2.ts @@ -0,0 +1,27 @@ +import path from "node:path"; +import { spawn } from "node:child_process"; +import type { ChildProcessWithoutNullStreams } from "node:child_process"; +import { app } from "electron"; + +export const startAria2 = (): Promise => { + return new Promise((resolve) => { + const binaryPath = app.isPackaged + ? path.join(process.resourcesPath, "aria2", "aria2c") + : path.join(__dirname, "..", "..", "aria2", "aria2c"); + + const cp = spawn(binaryPath, [ + "--enable-rpc", + "--rpc-listen-all", + "--file-allocation=none", + "--allow-overwrite=true", + ]); + + cp.stdout.on("data", async (data) => { + const msg = Buffer.from(data).toString("utf-8"); + + if (msg.includes("IPv6 RPC: listening on TCP")) { + resolve(cp); + } + }); + }); +}; diff --git a/src/main/services/download-manager.ts b/src/main/services/download-manager.ts index b14e0b14..9872a712 100644 --- a/src/main/services/download-manager.ts +++ b/src/main/services/download-manager.ts @@ -1,57 +1,39 @@ import Aria2, { StatusResponse } from "aria2"; -import { spawn } from "node:child_process"; import { gameRepository, userPreferencesRepository } from "@main/repository"; -import path from "node:path"; import { WindowManager } from "./window-manager"; import { RealDebridClient } from "./real-debrid"; -import { Notification, app } from "electron"; +import { Notification } from "electron"; import { t } from "i18next"; import { Downloader } from "@shared"; import { DownloadProgress } from "@types"; import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; import { Game } from "@main/entity"; +import { startAria2 } from "./aria2"; export class DownloadManager { private static downloads = new Map(); private static connected = false; private static gid: string | null = null; - private static gameId: number | null = null; + private static game: Game | null = null; + private static realDebridTorrentId: string | null = null; private static aria2 = new Aria2({}); - private static connect(): Promise { - return new Promise((resolve) => { - const binaryPath = app.isPackaged - ? path.join(process.resourcesPath, "aria2", "aria2c") - : path.join(__dirname, "..", "..", "aria2", "aria2c"); - - const cp = spawn(binaryPath, [ - "--enable-rpc", - "--rpc-listen-all", - "--file-allocation=none", - "--allow-overwrite=true", - ]); - - cp.stdout.on("data", async (data) => { - const msg = Buffer.from(data).toString("utf-8"); - - if (msg.includes("IPv6 RPC: listening on TCP")) { - await this.aria2.open(); - this.connected = true; - - resolve(true); - } - }); - }); + private static async connect() { + await startAria2(); + await this.aria2.open(); + this.connected = true; } - private static getETA(status: StatusResponse) { - const remainingBytes = - Number(status.totalLength) - Number(status.completedLength); - const speed = Number(status.downloadSpeed); + private static getETA( + totalLength: number, + completedLength: number, + speed: number + ) { + const remainingBytes = totalLength - completedLength; if (remainingBytes >= 0 && speed > 0) { return (remainingBytes / speed) * 1000; @@ -65,9 +47,7 @@ export class DownloadManager { where: { id: 1 }, }); - if (userPreferences?.downloadNotificationsEnabled && this.gameId) { - const game = await this.getGame(this.gameId); - + if (userPreferences?.downloadNotificationsEnabled && this.game) { new Notification({ title: t("download_complete", { ns: "notifications", @@ -76,7 +56,7 @@ export class DownloadManager { body: t("game_ready_to_install", { ns: "notifications", lng: userPreferences.language, - title: game?.title, + title: this.game.title, }), }).show(); } @@ -87,8 +67,73 @@ export class DownloadManager { return ""; } + private static async getRealDebridDownloadUrl() { + if (this.realDebridTorrentId) { + const torrentInfo = await RealDebridClient.getTorrentInfo( + this.realDebridTorrentId + ); + + const { status, links } = torrentInfo; + + if (status === "waiting_files_selection") { + await RealDebridClient.selectAllFiles(this.realDebridTorrentId); + return null; + } + + if (status === "downloaded") { + const [link] = links; + const { download } = await RealDebridClient.unrestrictLink(link); + return decodeURIComponent(download); + } + + if (WindowManager.mainWindow) { + const progress = torrentInfo.progress / 100; + const totalDownloaded = progress * torrentInfo.bytes; + + WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); + + const payload = { + numPeers: 0, + numSeeds: torrentInfo.seeders, + downloadSpeed: torrentInfo.speed, + timeRemaining: this.getETA( + torrentInfo.bytes, + totalDownloaded, + torrentInfo.speed + ), + isDownloadingMetadata: status === "magnet_conversion", + game: { + ...this.game, + bytesDownloaded: progress * torrentInfo.bytes, + progress, + }, + } as DownloadProgress; + + WindowManager.mainWindow.webContents.send( + "on-download-progress", + JSON.parse(JSON.stringify(payload)) + ); + } + } + + return null; + } + public static async watchDownloads() { - if (!this.gid || !this.gameId) return; + if (!this.game) return; + + if (!this.gid && this.realDebridTorrentId) { + const options = { dir: this.game.downloadPath! }; + const downloadUrl = await this.getRealDebridDownloadUrl(); + + if (downloadUrl) { + this.gid = await this.aria2.call("addUri", [downloadUrl], options); + this.downloads.set(this.game.id, this.gid); + this.realDebridTorrentId = null; + } + } + + if (!this.gid) return; const status = await this.aria2.call("tellStatus", this.gid); @@ -96,7 +141,7 @@ export class DownloadManager { if (status.followedBy?.length) { this.gid = status.followedBy[0]; - this.downloads.set(this.gameId, this.gid); + this.downloads.set(this.game.id, this.gid); return; } @@ -113,7 +158,7 @@ export class DownloadManager { if (!isNaN(progress)) update.progress = progress; await gameRepository.update( - { id: this.gameId }, + { id: this.game.id }, { ...update, status: status.status, @@ -123,17 +168,18 @@ export class DownloadManager { } const game = await gameRepository.findOne({ - where: { id: this.gameId, isDeleted: false }, + where: { id: this.game.id, isDeleted: false }, relations: { repack: true }, }); - if (progress === 1 && game && !isDownloadingMetadata) { + if (progress === 1 && this.game && !isDownloadingMetadata) { await this.publishNotification(); + /* Only cancel bittorrent downloads to stop seeding */ if (status.bittorrent) { - await this.cancelDownload(game.id); + await this.cancelDownload(this.game.id); } else { this.clearCurrentDownload(); } @@ -143,13 +189,14 @@ export class DownloadManager { WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); const payload = { - progress, - bytesDownloaded: Number(status.completedLength), - fileSize: Number(status.totalLength), numPeers: Number(status.connections), numSeeds: Number(status.numSeeders ?? 0), downloadSpeed: Number(status.downloadSpeed), - timeRemaining: this.getETA(status), + timeRemaining: this.getETA( + Number(status.totalLength), + Number(status.completedLength), + Number(status.downloadSpeed) + ), isDownloadingMetadata: !!isDownloadingMetadata, game, } as DownloadProgress; @@ -161,20 +208,12 @@ export class DownloadManager { } } - static async getGame(gameId: number) { - return gameRepository.findOne({ - where: { id: gameId, isDeleted: false }, - relations: { - repack: true, - }, - }); - } - private static clearCurrentDownload() { - if (this.gameId) { - this.downloads.delete(this.gameId); + if (this.game) { + this.downloads.delete(this.game.id); this.gid = null; - this.gameId = null; + this.game = null; + this.realDebridTorrentId = null; } } @@ -198,50 +237,42 @@ export class DownloadManager { if (this.gid) { await this.aria2.call("forcePause", this.gid); this.gid = null; - this.gameId = null; + this.game = null; + this.realDebridTorrentId = null; WindowManager.mainWindow?.setProgressBar(-1); } } - static async resumeDownload(gameId: number) { - if (this.downloads.has(gameId)) { - const gid = this.downloads.get(gameId)!; + static async resumeDownload(game: Game) { + if (this.downloads.has(game.id)) { + const gid = this.downloads.get(game.id)!; await this.aria2.call("unpause", gid); this.gid = gid; - this.gameId = gameId; + this.game = game; } else { - return this.startDownload(gameId); + return this.startDownload(game); } } - static async startDownload(gameId: number) { + static async startDownload(game: Game) { if (!this.connected) await this.connect(); - const game = await this.getGame(gameId)!; + const options = { + dir: game.downloadPath!, + }; - if (game) { - const options = { - dir: game.downloadPath!, - }; + if (game.downloader === Downloader.RealDebrid) { + this.realDebridTorrentId = await RealDebridClient.getTorrentId( + game!.repack.magnet + ); + } else { + this.gid = await this.aria2.call("addUri", [game.repack.magnet], options); - if (game.downloader === Downloader.RealDebrid) { - const downloadUrl = decodeURIComponent( - await RealDebridClient.getDownloadUrl(game) - ); - - this.gid = await this.aria2.call("addUri", [downloadUrl], options); - } else { - this.gid = await this.aria2.call( - "addUri", - [game.repack.magnet], - options - ); - } - - this.gameId = gameId; - this.downloads.set(gameId, this.gid); + this.downloads.set(game.id, this.gid); } + + this.game = game; } } diff --git a/src/main/services/real-debrid.ts b/src/main/services/real-debrid.ts index bf22e5ae..2e0debe6 100644 --- a/src/main/services/real-debrid.ts +++ b/src/main/services/real-debrid.ts @@ -1,5 +1,5 @@ -import { Game } from "@main/entity"; import axios, { AxiosInstance } from "axios"; +import parseTorrent from "parse-torrent"; import type { RealDebridAddMagnet, RealDebridTorrentInfo, @@ -7,10 +7,18 @@ import type { RealDebridUser, } from "@types"; -const base = "https://api.real-debrid.com/rest/1.0"; - export class RealDebridClient { private static instance: AxiosInstance; + private static baseURL = "https://api.real-debrid.com/rest/1.0"; + + static authorize(apiToken: string) { + this.instance = axios.create({ + baseURL: this.baseURL, + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + } static async addMagnet(magnet: string) { const searchParams = new URLSearchParams({ magnet }); @@ -23,7 +31,7 @@ export class RealDebridClient { return response.data; } - static async getInfo(id: string) { + static async getTorrentInfo(id: string) { const response = await this.instance.get( `/torrents/info/${id}` ); @@ -55,50 +63,24 @@ export class RealDebridClient { return response.data; } - static async getAllTorrentsFromUser() { + private static async getAllTorrentsFromUser() { const response = await this.instance.get("/torrents"); return response.data; } - static extractSHA1FromMagnet(magnet: string) { - return magnet.match(/btih:([0-9a-fA-F]*)/)?.[1].toLowerCase(); - } + static async getTorrentId(magnetUri: string) { + const userTorrents = await RealDebridClient.getAllTorrentsFromUser(); - static async getDownloadUrl(game: Game) { - const torrents = await RealDebridClient.getAllTorrentsFromUser(); - const hash = RealDebridClient.extractSHA1FromMagnet(game!.repack.magnet); - let torrent = torrents.find((t) => t.hash === hash); + const { infoHash } = await parseTorrent(magnetUri); + const userTorrent = userTorrents.find( + (userTorrent) => userTorrent.hash === infoHash + ); - // User haven't downloaded this torrent yet - if (!torrent) { - const magnet = await RealDebridClient.addMagnet(game!.repack.magnet); + if (userTorrent) return userTorrent.id; - if (magnet) { - await RealDebridClient.selectAllFiles(magnet.id); - torrent = await RealDebridClient.getInfo(magnet.id); - - const { links } = torrent; - const { download } = await RealDebridClient.unrestrictLink(links[0]); - - if (!download) { - throw new Error("Torrent not cached on Real Debrid"); - } - - return download; - } - } - - throw new Error(); - } - - static authorize(apiToken: string) { - this.instance = axios.create({ - baseURL: base, - headers: { - Authorization: `Bearer ${apiToken}`, - }, - }); + const torrent = await RealDebridClient.addMagnet(magnetUri); + return torrent.id; } } diff --git a/src/renderer/src/app.css.ts b/src/renderer/src/app.css.ts index f87b96a3..cadab243 100644 --- a/src/renderer/src/app.css.ts +++ b/src/renderer/src/app.css.ts @@ -26,9 +26,9 @@ globalStyle("body", { overflow: "hidden", userSelect: "none", fontFamily: "'Fira Mono', monospace", - fontSize: vars.size.bodyFontSize, + fontSize: vars.size.body, background: vars.color.background, - color: vars.color.bodyText, + color: vars.color.body, margin: "0", }); @@ -68,7 +68,7 @@ globalStyle( ); globalStyle("label", { - fontSize: vars.size.bodyFontSize, + fontSize: vars.size.body, }); globalStyle("input[type=number]", { diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 28abbd71..325e1506 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -134,17 +134,18 @@ export function App() {
- -
+ + + ); } diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.css.ts b/src/renderer/src/components/bottom-panel/bottom-panel.css.ts index 39aef69f..22f71fe4 100644 --- a/src/renderer/src/components/bottom-panel/bottom-panel.css.ts +++ b/src/renderer/src/components/bottom-panel/bottom-panel.css.ts @@ -4,6 +4,7 @@ import { SPACING_UNIT, vars } from "../../theme.css"; export const bottomPanel = style({ width: "100%", borderTop: `solid 1px ${vars.color.border}`, + backgroundColor: vars.color.background, padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 2}px`, display: "flex", alignItems: "center", @@ -14,10 +15,10 @@ export const bottomPanel = style({ }); export const downloadsButton = style({ - color: vars.color.bodyText, + color: vars.color.body, borderBottom: "1px solid transparent", ":hover": { - borderBottom: `1px solid ${vars.color.bodyText}`, + borderBottom: `1px solid ${vars.color.body}`, cursor: "pointer", }, }); diff --git a/src/renderer/src/components/game-card/game-card.css.ts b/src/renderer/src/components/game-card/game-card.css.ts index 1f45c106..3047ce2f 100644 --- a/src/renderer/src/components/game-card/game-card.css.ts +++ b/src/renderer/src/components/game-card/game-card.css.ts @@ -109,6 +109,6 @@ export const shopIcon = style({ }); export const noDownloadsLabel = style({ - color: vars.color.bodyText, + color: vars.color.body, fontWeight: "bold", }); diff --git a/src/renderer/src/components/header/header.css.ts b/src/renderer/src/components/header/header.css.ts index 705b533e..a1ee5469 100644 --- a/src/renderer/src/components/header/header.css.ts +++ b/src/renderer/src/components/header/header.css.ts @@ -108,7 +108,7 @@ export const section = style({ export const backButton = recipe({ base: { - color: vars.color.bodyText, + color: vars.color.body, cursor: "pointer", WebkitAppRegion: "no-drag", position: "absolute", diff --git a/src/renderer/src/components/modal/modal.css.ts b/src/renderer/src/components/modal/modal.css.ts index 89e17b0e..557b5a0a 100644 --- a/src/renderer/src/components/modal/modal.css.ts +++ b/src/renderer/src/components/modal/modal.css.ts @@ -23,7 +23,7 @@ export const modal = recipe({ backgroundColor: vars.color.background, borderRadius: "5px", maxWidth: "600px", - color: vars.color.bodyText, + color: vars.color.body, maxHeight: "100%", border: `solid 1px ${vars.color.border}`, overflow: "hidden", @@ -65,5 +65,5 @@ export const closeModalButton = style({ }); export const closeModalButtonIcon = style({ - color: vars.color.bodyText, + color: vars.color.body, }); diff --git a/src/renderer/src/components/toast/toast.css.ts b/src/renderer/src/components/toast/toast.css.ts index eebe7bda..5cba3fdd 100644 --- a/src/renderer/src/components/toast/toast.css.ts +++ b/src/renderer/src/components/toast/toast.css.ts @@ -25,12 +25,13 @@ export const toast = recipe({ borderRadius: "4px", border: `solid 1px ${vars.color.border}`, left: "50%", - /* Bottom panel height + spacing */ + /* Bottom panel height + 16px */ bottom: `${26 + SPACING_UNIT * 2}px`, overflow: "hidden", display: "flex", flexDirection: "column", justifyContent: "space-between", + zIndex: "0", }, variants: { closing: { @@ -66,7 +67,7 @@ export const progress = style({ }); export const closeButton = style({ - color: vars.color.bodyText, + color: vars.color.body, cursor: "pointer", padding: "0", margin: "0", diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 638fb917..ca86e95d 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -22,7 +22,7 @@ declare global { interface Electron { /* Torrenting */ - startGameDownload: (payload: StartGameDownloadPayload) => Promise; + startGameDownload: (payload: StartGameDownloadPayload) => Promise; cancelGameDownload: (gameId: number) => Promise; pauseGameDownload: (gameId: number) => Promise; resumeGameDownload: (gameId: number) => Promise; diff --git a/src/renderer/src/features/toast-slice.ts b/src/renderer/src/features/toast-slice.ts index 192fb1fc..112f641f 100644 --- a/src/renderer/src/features/toast-slice.ts +++ b/src/renderer/src/features/toast-slice.ts @@ -19,7 +19,6 @@ export const toastSlice = createSlice({ initialState, reducers: { showToast: (state, action: PayloadAction>) => { - console.log(action.payload); state.message = action.payload.message; state.visible = true; }, diff --git a/src/renderer/src/pages/downloads/downloads.css.ts b/src/renderer/src/pages/downloads/downloads.css.ts index 4df3dc90..913bec47 100644 --- a/src/renderer/src/pages/downloads/downloads.css.ts +++ b/src/renderer/src/pages/downloads/downloads.css.ts @@ -12,7 +12,7 @@ export const downloadTitleWrapper = style({ export const downloadTitle = style({ fontWeight: "bold", cursor: "pointer", - color: vars.color.bodyText, + color: vars.color.body, textAlign: "left", fontSize: "16px", display: "block", diff --git a/src/renderer/src/pages/game-details/game-details.css.ts b/src/renderer/src/pages/game-details/game-details.css.ts index fd55d69d..7437960b 100644 --- a/src/renderer/src/pages/game-details/game-details.css.ts +++ b/src/renderer/src/pages/game-details/game-details.css.ts @@ -174,5 +174,5 @@ globalStyle(`${description} img`, { }); globalStyle(`${description} a`, { - color: vars.color.bodyText, + color: vars.color.body, }); diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.css.ts b/src/renderer/src/pages/game-details/modals/repacks-modal.css.ts index 11fc71f6..897d88f0 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.css.ts +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.css.ts @@ -13,6 +13,6 @@ export const repackButton = style({ flexDirection: "column", alignItems: "flex-start", gap: `${SPACING_UNIT}px`, - color: vars.color.bodyText, + color: vars.color.body, padding: `${SPACING_UNIT * 2}px`, }); diff --git a/src/renderer/src/pages/game-details/modals/select-folder-modal.css.tsx b/src/renderer/src/pages/game-details/modals/select-folder-modal.css.tsx index e63603c2..b5bd4b56 100644 --- a/src/renderer/src/pages/game-details/modals/select-folder-modal.css.tsx +++ b/src/renderer/src/pages/game-details/modals/select-folder-modal.css.tsx @@ -15,7 +15,7 @@ export const downloadsPathField = style({ export const hintText = style({ fontSize: "12px", - color: vars.color.bodyText, + color: vars.color.body, }); export const downloaders = style({ diff --git a/src/renderer/src/pages/game-details/modals/select-folder-modal.tsx b/src/renderer/src/pages/game-details/modals/select-folder-modal.tsx index 6825f32b..71fae213 100644 --- a/src/renderer/src/pages/game-details/modals/select-folder-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/select-folder-modal.tsx @@ -125,7 +125,7 @@ export function SelectFolderModal({ {selectedDownloader === Downloader.RealDebrid && ( )} - Real Debrid + Real-Debrid diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts b/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts index cae988ab..e6d8b60a 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts @@ -88,5 +88,5 @@ export const howLongToBeatCategorySkeleton = style({ globalStyle(`${requirementsDetails} a`, { display: "flex", - color: vars.color.bodyText, + color: vars.color.body, }); diff --git a/src/renderer/src/pages/settings/settings-real-debrid.css.ts b/src/renderer/src/pages/settings/settings-real-debrid.css.ts index 73763780..dba8565b 100644 --- a/src/renderer/src/pages/settings/settings-real-debrid.css.ts +++ b/src/renderer/src/pages/settings/settings-real-debrid.css.ts @@ -7,3 +7,8 @@ export const form = style({ flexDirection: "column", gap: `${SPACING_UNIT}px`, }); + +export const description = style({ + fontFamily: "'Fira Sans', sans-serif", + marginBottom: `${SPACING_UNIT}px`, +}); diff --git a/src/renderer/src/pages/settings/settings-real-debrid.tsx b/src/renderer/src/pages/settings/settings-real-debrid.tsx index 67525a71..22957da7 100644 --- a/src/renderer/src/pages/settings/settings-real-debrid.tsx +++ b/src/renderer/src/pages/settings/settings-real-debrid.tsx @@ -52,8 +52,6 @@ export function SettingsRealDebrid({ form.realDebridApiToken! ); - console.log(user); - if (user.type === "premium") { dispatch( showToast({ @@ -61,18 +59,22 @@ export function SettingsRealDebrid({ type: "success", }) ); + + updateUserPreferences({ + realDebridApiToken: form.useRealDebrid + ? form.realDebridApiToken + : null, + }); } } - - // updateUserPreferences({ - // realDebridApiToken: form.useRealDebrid ? form.realDebridApiToken : null, - // }); }; const isButtonDisabled = form.useRealDebrid && !form.realDebridApiToken; return (
+

{t("real_debrid_description")}

+ diff --git a/src/renderer/src/theme.css.ts b/src/renderer/src/theme.css.ts index 0bb34aa5..125e1543 100644 --- a/src/renderer/src/theme.css.ts +++ b/src/renderer/src/theme.css.ts @@ -7,7 +7,7 @@ export const [themeClass, vars] = createTheme({ background: "#1c1c1c", darkBackground: "#151515", muted: "#c0c1c7", - bodyText: "#8e919b", + body: "#8e919b", border: "#424244", success: "#1c9749", danger: "#e11d48", @@ -17,6 +17,6 @@ export const [themeClass, vars] = createTheme({ active: "0.7", }, size: { - bodyFontSize: "14px", + body: "14px", }, }); diff --git a/src/types/index.ts b/src/types/index.ts index 5de56ee9..11902745 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -115,9 +115,6 @@ export interface DownloadProgress { numPeers: number; numSeeds: number; isDownloadingMetadata: boolean; - progress: number; - bytesDownloaded: number; - fileSize: number; game: Omit; } @@ -197,7 +194,18 @@ export interface RealDebridTorrentInfo { host: string; split: number; progress: number; - status: string; + status: + | "magnet_error" + | "magnet_conversion" + | "waiting_files_selection" + | "queued" + | "downloading" + | "downloaded" + | "error" + | "virus" + | "compressing" + | "uploading" + | "dead"; added: string; files: { id: number;