diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 431df932..c9094117 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,17 +22,6 @@ jobs: - name: Install dependencies run: yarn - - name: Install Python - uses: actions/setup-python@v5 - with: - python-version: 3.9 - - - name: Install dependencies - run: pip install -r requirements.txt - - - name: Build with cx_Freeze - run: python torrent-client/setup.py build - - name: Build Linux if: matrix.os == 'ubuntu-latest' run: yarn build:linux diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc65189e..cd91c635 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,17 +24,6 @@ jobs: - name: Install dependencies run: yarn - - name: Install Python - uses: actions/setup-python@v5 - with: - python-version: 3.9 - - - name: Install dependencies - run: pip install -r requirements.txt - - - name: Build with cx_Freeze - run: python torrent-client/setup.py build - - name: Build Linux if: matrix.os == 'ubuntu-latest' run: yarn build:linux diff --git a/.gitignore b/.gitignore index b7bedf97..7bd76930 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .vscode node_modules -hydra-download-manager +aria2/ fastlist.exe __pycache__ dist diff --git a/electron-builder.yml b/electron-builder.yml index a90fa394..3b35fd18 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -3,10 +3,10 @@ productName: Hydra directories: buildResources: build extraResources: - - hydra-download-manager + - aria2 + - seeds - hydra.db - fastlist.exe - - seeds files: - "!**/.vscode/*" - "!src/*" diff --git a/hydra.db b/hydra.db index dad57120..49089bb3 100644 Binary files a/hydra.db and b/hydra.db differ diff --git a/package.json b/package.json index 4a9b014b..99962918 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "description": "Hydra", "main": "./out/main/index.js", "author": "Los Broxas", - "homepage": "https://hydralauncher.site", "repository": { "type": "git", "url": "https://github.com/hydralauncher/hydra.git" @@ -42,6 +41,7 @@ "@vanilla-extract/css": "^1.14.2", "@vanilla-extract/recipes": "^0.5.2", "iso-639-1": "3.1.2", + "aria2": "^4.1.2", "auto-launch": "^5.0.6", "axios": "^1.6.8", "better-sqlite3": "^9.5.0", @@ -50,7 +50,6 @@ "color": "^4.2.3", "color.js": "^1.2.0", "date-fns": "^3.6.0", - "easydl": "^1.1.1", "electron-log": "^5.1.4", "electron-updater": "^6.1.8", "fetch-cookie": "^3.0.1", @@ -60,7 +59,6 @@ "jsdom": "^24.0.0", "lodash-es": "^4.17.21", "lottie-react": "^2.4.0", - "node-7z-archive": "^1.1.7", "parse-torrent": "^11.0.16", "ps-list": "^8.1.1", "react-i18next": "^14.1.0", diff --git a/postinstall.cjs b/postinstall.cjs index 8ca8f101..2b793257 100644 --- a/postinstall.cjs +++ b/postinstall.cjs @@ -1,4 +1,51 @@ -const fs = require("fs"); +const { default: axios } = require("axios"); +const util = require("node:util"); +const fs = require("node:fs"); + +const exec = util.promisify(require("node:child_process").exec); + +const downloadAria2 = async () => { + if (fs.existsSync("aria2")) { + console.log("Aria2 already exists, skipping download..."); + return; + } + + const file = + process.platform === "win32" + ? "aria2-1.37.0-win-64bit-build1.zip" + : "aria2-1.37.0-1-x86_64.pkg.tar.zst"; + + const downloadUrl = + process.platform === "win32" + ? `https://github.com/aria2/aria2/releases/download/release-1.37.0/${file}` + : "https://archlinux.org/packages/extra/x86_64/aria2/download/"; + + console.log(`Downloading ${file}...`); + + const response = await axios.get(downloadUrl, { responseType: "stream" }); + + const stream = response.data.pipe(fs.createWriteStream(file)); + + stream.on("finish", async () => { + console.log(`Downloaded ${file}, extracting...`); + + if (process.platform === "win32") { + await exec(`npx extract-zip ${file}`); + console.log("Extracted. Renaming folder..."); + + fs.renameSync(file.replace(".zip", ""), "aria2"); + } else { + await exec(`tar --zstd -xvf ${file} usr/bin/aria2c`); + console.log("Extracted. Copying binary file..."); + fs.mkdirSync("aria2"); + fs.copyFileSync("usr/bin/aria2c", "aria2/aria2c"); + fs.rmSync("usr", { recursive: true }); + } + + console.log(`Extracted ${file}, removing compressed downloaded file...`); + fs.rmSync(file); + }); +}; if (process.platform === "win32") { fs.copyFileSync( @@ -6,3 +53,5 @@ if (process.platform === "win32") { "fastlist.exe" ); } + +downloadAria2(); diff --git a/src/locales/ar/translation.json b/src/locales/ar/translation.json index 3ea46b4f..09547da8 100644 --- a/src/locales/ar/translation.json +++ b/src/locales/ar/translation.json @@ -12,7 +12,6 @@ "settings": "إعدادات", "my_library": "مكتبتي", "downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)", - "checking_files": "{{title}} ({{percentage}} - جارٍ التحقق من الملفات...)", "paused": "{{title}} (متوقف)", "downloading": "{{title}} ({{percentage}} - جارٍ التنزيل...)", "filter": "بحث في المكتبة", @@ -34,7 +33,6 @@ "bottom_panel": { "no_downloads_in_progress": "لا يوجد تنزيلات جارية", "downloading_metadata": "جارٍ تنزيل بيانات وصف {{title}}", - "checking_files": "جارٍ التحقق من ملفات {{title}}… ({{percentage}} مكتملة)", "downloading": "جارٍ تنزيل {{title}}… ({{percentage}} مكتملة) - الانتهاء {{eta}} - {{speed}}" }, "catalogue": { @@ -56,14 +54,12 @@ "space_left_on_disk": "{{space}} متبقية على القرص", "eta": "الوقت المتبقي {{eta}}", "downloading_metadata": "جاري تنزيل البيانات الوصفية...", - "checking_files": "جاري التحقق من الملفات...", "filter": "تصفية حزم إعادة التجميع", "requirements": "متطلبات النظام", "minimum": "الحد الأدنى", "recommended": "موصى به", "no_minimum_requirements": "{{title}} لا تتوفر معلومات عن الحد الأدنى للمتطلبات", "no_recommended_requirements": "{{title}} لا تتوفر معلومات عن المتطلبات الموصى بها", - "paused_progress": "{{progress}} (متوقف)", "release_date": "تم الإصدار في {{date}}", "publisher": "نشر بواسطة {{publisher}}", "copy_link_to_clipboard": "نسخ الرابط", @@ -120,22 +116,18 @@ "verifying": "جار التحقق…", "completed_at": "اكتمل في {{date}}", "completed": "اكتمل", - "cancelled": "ملغي", "download_again": "تحميل مرة أخرى", "cancel": "إلغاء", "filter": "تصفية الألعاب التي تم تنزيلها", "remove": "إزالة", "downloading_metadata": "جار تنزيل البيانات الوصفية…", - "checking_files": "جار التحقق من الملفات…", "starting_download": "يبدأ التنزيل…", "deleting": "جار حذف المثبت…", "delete": "إزالة المثبت", "remove_from_list": "إزالة", "delete_modal_title": "هل أنت متأكد؟", "delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهاز الكمبيوتر الخاص بك", - "install": "تثبيت", - "real_debrid": "Real Debrid", - "torrent": "تورنت" + "install": "تثبيت" }, "settings": { "downloads_path": "مسار التنزيلات", @@ -145,14 +137,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", - "real_debrid_api_token_hint": "يمكنك الحصول على مفتاح API الخاص بك هنا.", + "enable_real_debrid": "تفعيل Real-Debrid ", + "real_debrid_api_token_hint": "يمكنك الحصول على مفتاح API الخاص بك هنا", "save_changes": "حفظ التغييرات" }, "notifications": { diff --git a/src/locales/be/translation.json b/src/locales/be/translation.json index 0053795a..617a444a 100644 --- a/src/locales/be/translation.json +++ b/src/locales/be/translation.json @@ -12,7 +12,6 @@ "settings": "Налады", "my_library": "Мая бібліятэка", "downloading_metadata": "{{title}} (Сцягванне мэтаданых…)", - "checking_files": "{{title}} ({{percentage}} - Праверка файлаў…)", "paused": "{{title}} (Спынена)", "downloading": "{{title}} ({{percentage}} - Сцягванне…)", "filter": "Фільтар бібліятэкі", @@ -34,7 +33,6 @@ "bottom_panel": { "no_downloads_in_progress": "Няма актыўных сцягванняў", "downloading_metadata": "Сцягванне мэтаданых {{title}}…", - "checking_files": "Праверка файлаў {{title}}… ({{percentage}} скончана)", "downloading": "Сцягванне {{title}}… ({{percentage}} скончана) - Канчатак {{eta}} - {{speed}}" }, "catalogue": { @@ -56,14 +54,12 @@ "space_left_on_disk": "{{space}} засталося на дыску", "eta": "Канчатак {{eta}}", "downloading_metadata": "Сцягванне мэтаданых…", - "checking_files": "Праверка файлаў…", "filter": "Фільтар рэпакаў", "requirements": "Сістэмныя патрэбаванни", "minimum": "Мінімальныя", "recommended": "Рэкамендуемыя", "no_minimum_requirements": "{{title}} ня ўтрымлівае інфармацыі пра мінімальныя патрабаванні", "no_recommended_requirements": "{{title}} ня ўтрымлівае інфармацыі пра рэкамендуемыя патрабаванні", - "paused_progress": "{{progress}} (Спынена)", "release_date": "Выпушчана {{date}}", "publisher": "Выдана {{publisher}}", "copy_link_to_clipboard": "Скапіяваць спасылку", @@ -86,7 +82,6 @@ "playing_now": "Зараз гуляе", "change": "Змяніць", "repacks_modal_description": "Абярыце рэпак, які хочаце сцягнуць", - "downloads_path": "Шлях сцягвання", "select_folder_hint": "Каб змяніць папку па змоўчанні, адкрыйце", "download_now": "Сцягнуць зараз", "installation_instructions": "Інструкцыя ўсталёўкі", @@ -114,13 +109,11 @@ "verifying": "Праверка…", "completed_at": "Скончана а {{date}}", "completed": "Скончана", - "cancelled": "Скасавана", "download_again": "Сцягнуць зноў", "cancel": "Скасаваць", "filter": "Фільтар сцягнутых гульняў", "remove": "Выдаліць", "downloading_metadata": "Сцягванне мэтаданых…", - "checking_files": "Праверка файлаў…", "starting_download": "Пачатак сцягвання…", "deleting": "Выдаленне ўсталёўшчыка…", "delete": "Выдаліць усталёўшчык", diff --git a/src/locales/da/translation.json b/src/locales/da/translation.json index dca04160..67cab4a7 100644 --- a/src/locales/da/translation.json +++ b/src/locales/da/translation.json @@ -12,7 +12,6 @@ "settings": "Indstillinger", "my_library": "Mit bibliotek", "downloading_metadata": "{{title}} (Downloader metadata…)", - "checking_files": "{{title}} ({{percentage}} - Tjekker filer…)", "paused": "{{title}} (Paused)", "downloading": "{{title}} ({{percentage}} - Downloading…)", "filter": "Filtrer bibliotek", @@ -34,7 +33,6 @@ "bottom_panel": { "no_downloads_in_progress": "Ingen downloads igang", "downloading_metadata": "Downloader {{title}} metadata…", - "checking_files": "Tjekker {{title}} filer… ({{percentage}} færdig)", "downloading": "Downloader {{title}}… ({{percentage}} færdig) - Konklusion {{eta}} - {{speed}}" }, "catalogue": { @@ -56,14 +54,12 @@ "space_left_on_disk": "{{space}} tilbage på harddisken", "eta": "Konklusion {{eta}}", "downloading_metadata": "Downloader metadata…", - "checking_files": "Tjekker filer…", "filter": "Filtrer repacks", "requirements": "System behov", "minimum": "Mindste", "recommended": "Anbefalet", "no_minimum_requirements": "{{title}} angiver ikke mindste behov informationer", "no_recommended_requirements": "{{title}} angiver ikke anbefalet behov informationer", - "paused_progress": "{{progress}} (Pauset)", "release_date": "Offentliggjort den {{date}}", "publisher": "Udgivet af {{publisher}}", "copy_link_to_clipboard": "Kopier link", @@ -86,7 +82,6 @@ "playing_now": "Spiller nu", "change": "Ændré", "repacks_modal_description": "Vælg den repack du vil downloade", - "downloads_path": "Downloads sti", "select_folder_hint": "For at ændre standard mappen, gå til <0>Instillingerne", "download_now": "Download nu", "installation_instructions": "Installations Instrukser", @@ -114,22 +109,18 @@ "verifying": "Verificerer…", "completed_at": "Færdiggjort på {{date}}", "completed": "Færdigt", - "cancelled": "Annulleret", "download_again": "Download igen", "cancel": "Annullér", "filter": "Filtrer downloadet spil", "remove": "Fjern", "downloading_metadata": "Downloader metadata…", - "checking_files": "Tjekker filer…", "starting_download": "Starter download…", "deleting": "Sletter installatør…", "delete": "Fjern installatør", "remove_from_list": "Fjern", "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", - "torrent": "Torrent" + "install": "Installér" }, "settings": { "downloads_path": "Downloads sti", @@ -139,14 +130,12 @@ "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", "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", - "real_debrid_api_token_hint": "Du kan få din API nøgle <0>her.", + "enable_real_debrid": "Slå Real-Debrid til", + "real_debrid_api_token_hint": "Du kan få din API nøgle <0>her", "save_changes": "Gem ændringer" }, "notifications": { diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 08dc5b53..643445e0 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -12,7 +12,6 @@ "settings": "Settings", "my_library": "My library", "downloading_metadata": "{{title}} (Downloading metadata…)", - "checking_files": "{{title}} ({{percentage}} - Checking files…)", "paused": "{{title}} (Paused)", "downloading": "{{title}} ({{percentage}} - Downloading…)", "filter": "Filter library", @@ -29,13 +28,14 @@ "catalogue": "Catalogue", "downloads": "Downloads", "search_results": "Search results", - "settings": "Settings" + "settings": "Settings", + "version_available": "Version {{version}} available. Click here to restart and install." }, "bottom_panel": { "no_downloads_in_progress": "No downloads in progress", "downloading_metadata": "Downloading {{title}} metadata…", - "checking_files": "Checking {{title}} files… ({{percentage}} complete)", - "downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}" + "downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}", + "calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…" }, "catalogue": { "next_page": "Next page", @@ -55,15 +55,15 @@ "remove_from_list": "Remove", "space_left_on_disk": "{{space}} left on disk", "eta": "Conclusion {{eta}}", + "calculating_eta": "Calculating remaining time…", "downloading_metadata": "Downloading metadata…", - "checking_files": "Checking files…", "filter": "Filter repacks", "requirements": "System requirements", "minimum": "Minimum", "recommended": "Recommended", "no_minimum_requirements": "{{title}} doesn't provide minimum requirements information", "no_recommended_requirements": "{{title}} doesn't provide recommended requirements information", - "paused_progress": "{{progress}} (Paused)", + "paused": "Paused", "release_date": "Released on {{date}}", "publisher": "Published by {{publisher}}", "copy_link_to_clipboard": "Copy link", @@ -102,7 +102,9 @@ "previous_screenshot": "Previous screenshot", "next_screenshot": "Next screenshot", "screenshot": "Screenshot {{number}}", - "open_screenshot": "Open screenshot {{number}}" + "open_screenshot": "Open screenshot {{number}}", + "download_settings": "Download settings", + "downloader": "Downloader" }, "activation": { "title": "Activate Hydra", @@ -120,22 +122,19 @@ "verifying": "Verifying…", "completed_at": "Completed in {{date}}", "completed": "Completed", - "cancelled": "Cancelled", + "removed": "Not downloaded", "download_again": "Download again", "cancel": "Cancel", "filter": "Filter downloaded games", "remove": "Remove", "downloading_metadata": "Downloading metadata…", - "checking_files": "Checking files…", "starting_download": "Starting download…", "deleting": "Deleting installer…", "delete": "Remove installer", "remove_from_list": "Remove", "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", - "torrent": "Torrent" + "install": "Install" }, "settings": { "downloads_path": "Downloads path", @@ -145,16 +144,21 @@ "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", - "quit_app_instead_hiding": "Quit Hydra instead of minimizing to tray", + "real_debrid_api_token_label": "Real-Debrid API token", + "quit_app_instead_hiding": "Don't hide Hydra when closing", "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.", + "language": "Language", + "real_debrid_api_token": "API Token", + "enable_real_debrid": "Enable 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_invalid_token": "Invalid API token", + "real_debrid_api_token_hint": "You can get your API token <0>here", + "real_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to Real-Debrid", + "real_debrid_linked_message": "Account \"{{username}}\" linked", "save_changes": "Save changes", - "language": "Language" + "changes_saved": "Changes successfully saved" }, "notifications": { "download_complete": "Download complete", @@ -177,11 +181,5 @@ }, "modal": { "close": "Close button" - }, - "splash": { - "downloading_version": "Downloading version {{version}}", - "searching_updates": "Searching for updates", - "update_found": "Update {{version}} found", - "restarting_and_applying": "Restarting and applying update" } } diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index b200c7b8..e30f088a 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -12,7 +12,6 @@ "settings": "Ajustes", "my_library": "Mi biblioteca", "downloading_metadata": "{{title}} (Descargando metadatos…)", - "checking_files": "{{title}} ({{percentage}} - Analizando archivos…)", "paused": "{{title}} (Pausado)", "downloading": "{{title}} ({{percentage}} - Descargando…)", "filter": "Buscar en la biblioteca", @@ -29,12 +28,12 @@ "catalogue": "Catálogo", "downloads": "Descargas", "search_results": "Resultados de búsqueda", - "settings": "Ajustes" + "settings": "Ajustes", + "version_available": "Version {{version}} disponible. Haga clic aquí para reiniciar e instalar." }, "bottom_panel": { "no_downloads_in_progress": "Sin descargas en progreso", "downloading_metadata": "Descargando metadatos de {{title}}…", - "checking_files": "Analizando archivos de {{title}} - ({{percentage}} completado)", "downloading": "Descargando {{title}}… ({{percentage}} completado) - Finalizando {{eta}} - {{speed}}" }, "catalogue": { @@ -56,14 +55,12 @@ "space_left_on_disk": "{{space}} restantes en el disco", "eta": "Tiempo restante: {{eta}}", "downloading_metadata": "Descargando metadatos…", - "checking_files": "Analizando archivos…", "filter": "Buscar repacks", "requirements": "Requisitos del Sistema", "minimum": "Mínimos", "recommended": "Recomendados", "no_minimum_requirements": "Sin requisitos mínimos para {{title}}", "no_recommended_requirements": "{{title}} no tiene requisitos recomendados", - "paused_progress": "{{progress}} (Pausado)", "release_date": "Fecha de lanzamiento: {{date}}", "publisher": "Publicado por: {{publisher}}", "copy_link_to_clipboard": "Copiar enlace", @@ -120,22 +117,18 @@ "verifying": "Verificando…", "completed_at": "Completado el {{date}}", "completed": "Completado", - "cancelled": "Cancelado", "download_again": "Descargar de nuevo", "cancel": "Cancelar", "filter": "Buscar juegos descargados", "remove": "Eliminar", "downloading_metadata": "Descargando metadatos…", - "checking_files": "Verificando archivos…", "starting_download": "Iniciando descarga…", "deleting": "Eliminando instalador…", "delete": "Eliminar instalador", "remove_from_list": "Eliminar", "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", - "torrent": "Torrent" + "install": "Instalar" }, "settings": { "downloads_path": "Ruta de descarga", @@ -145,16 +138,15 @@ "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", - "real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí.", - "save_changes": "Guardar cambios", - "language": "Idioma" + "language": "Idioma", + "enable_real_debrid": "Activar Real-Debrid", + "real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí", + "save_changes": "Guardar cambios" }, "notifications": { "download_complete": "Descarga completada", @@ -177,11 +169,5 @@ }, "modal": { "close": "Botón de cierre" - }, - "splash": { - "downloading_version": "Descargando versión {{version}}", - "searching_updates": "Buscando actualizaciones", - "update_found": "Actualización {{version}} encontrada", - "restarting_and_applying": "Reiniciando y aplicando actualización" } } diff --git a/src/locales/fa/translation.json b/src/locales/fa/translation.json index aa929945..e2ea2974 100644 --- a/src/locales/fa/translation.json +++ b/src/locales/fa/translation.json @@ -12,7 +12,6 @@ "settings": "تنظیمات", "my_library": "کتابخانه‌ی من", "downloading_metadata": "{{title}} (در حال دانلود متادیتا...)", - "checking_files": "{{title}} ({{percentage}} - در حال بررسی فایل‌ها...)", "paused": "{{title}} (متوقف شده)", "downloading": "{{title}} ({{percentage}} - در حال دانلود…)", "filter": "فیلتر کردن کتابخانه", @@ -34,7 +33,6 @@ "bottom_panel": { "no_downloads_in_progress": "دانلودی در حال انجام نیست", "downloading_metadata": "درحال دانلود متادیتاهای {{title}}…", - "checking_files": "در حال چک کردن فایل‌های {{title}}…. ({{percentage}} تکمیل شده)", "downloading": "در حال دانلود {{title}}… ({{percentage}} تکمیل شده) - اتمام {{eta}} - {{speed}}" }, "catalogue": { @@ -56,14 +54,12 @@ "space_left_on_disk": "{{space}} فضا در دیسک باقی‌مانده", "eta": "اتمام {{eta}}", "downloading_metadata": "در حال دانلود متادیتاها…", - "checking_files": "در حال چک کردن فایل‌ها", "filter": "فیلترکردن ریپک‌ها", "requirements": "سیستم مورد نیاز", "minimum": "حداقل", "recommended": "پیشنهادی", "no_minimum_requirements": "{{title}} اطلاعات حداقل سیستم مورد نیاز را فراهم نکرده", "no_recommended_requirements": "{{title}} اطلاعات پیشنهادی سیستم مورد نیاز را فراهم نکرده", - "paused_progress": "{{progress}} (متوقف شده)", "release_date": "منتشر شده در {{date}}", "publisher": "منتشر شده توسط {{publisher}}", "copy_link_to_clipboard": "کپی لینک", @@ -86,7 +82,6 @@ "playing_now": "در حال بازی", "change": "تغییر", "repacks_modal_description": "ریپک مورد نظر برای دانلود را انتخاب کنید", - "downloads_path": "آدرس دانلودها", "select_folder_hint": "برای تغییر پوشه‌ی پیش‌فرض به <0>Settings بروید", "download_now": "الان دانلود کن", "installation_instructions": "دستورات نصب", @@ -114,22 +109,18 @@ "verifying": "در حال اعتبارسنجی…", "completed_at": "پایان یافته در {{date}}", "completed": "پایان یافته", - "cancelled": "لغو شده", "download_again": "دانلود مجدد", "cancel": "لغو", "filter": "فیلتر بازی‌های دانلود شده", "remove": "حذف", "downloading_metadata": "در حال دانلود متادیتاها…", - "checking_files": "در حال چک کردن فایل‌ها…", "starting_download": "در حال آغار دانلود…", "deleting": "در حال پاک کردن اینستالر…", "delete": "پاک کردن", "remove_from_list": "حذف", "delete_modal_title": "مطمئنی؟", "delete_modal_description": "این کار تمام فایل‌های اینستالر را از کامپیوتر شما حذف می‌کند", - "install": "نصف", - "real_debrid": "Real Debrid", - "torrent": "تورنت" + "install": "نصف" }, "settings": { "downloads_path": "مسیر دانلودها", @@ -139,13 +130,11 @@ "enable_repack_list_notifications": "زمانی که یک ریپک جدید اضافه شد", "telemetry": "تلمتری", "telemetry_description": "فعال کردن آمارگیری استفاده ناشناس", - "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_api_token_hint": "کلید API خود را از <ب0>اینجا بگیرید.", "save_changes": "ذخیره تغییرات" }, diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index f9bdde3d..352128f7 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -12,7 +12,6 @@ "settings": "Paramètres", "my_library": "Ma bibliothèque", "downloading_metadata": "{{title}} (Téléchargement des métadonnées…)", - "checking_files": "{{title}} ({{percentage}} - Vérification des fichiers…)", "paused": "{{title}} (En pause)", "downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)", "filter": "Filtrer la bibliothèque", @@ -30,7 +29,6 @@ "bottom_panel": { "no_downloads_in_progress": "Aucun téléchargement en cours", "downloading_metadata": "Téléchargement des métadonnées de {{title}}…", - "checking_files": "Vérification des fichiers de {{title}}… ({{percentage}} terminé)", "downloading": "Téléchargement de {{title}}… ({{percentage}} terminé) - Fin dans {{eta}} - {{speed}}" }, "game_details": { @@ -47,14 +45,12 @@ "space_left_on_disk": "{{space}} restant sur le disque", "eta": "Fin dans {{eta}}", "downloading_metadata": "Téléchargement des métadonnées en cours…", - "checking_files": "Vérification des fichiers…", "filter": "Filtrer les repacks", "requirements": "Configuration requise", "minimum": "Minimum", "recommended": "Recommandée", "no_minimum_requirements": "{{title}} ne fournit pas d'informations sur les configurations minimales", "no_recommended_requirements": "{{title}} ne fournit pas d'informations sur les configurations recommandées", - "paused_progress": "{{progress}} (En pause)", "release_date": "Sorti le {{date}}", "publisher": "Édité par {{publisher}}", "copy_link_to_clipboard": "Copier le lien", @@ -93,13 +89,11 @@ "verifying": "Vérification en cours…", "completed_at": "Terminé en {{date}}", "completed": "Terminé", - "cancelled": "Annulé", "download_again": "Télécharger à nouveau", "cancel": "Annuler", "filter": "Filtrer les jeux téléchargés", "remove": "Supprimer", "downloading_metadata": "Téléchargement des métadonnées en cours…", - "checking_files": "Vérification des fichiers…", "starting_download": "Démarrage du téléchargement…", "remove_from_list": "Retirer", "delete": "Supprimer le programme d'installation", diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index 8a370fb2..0800f1f9 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -12,7 +12,6 @@ "settings": "Beállítások", "my_library": "Könyvtáram", "downloading_metadata": "{{title}} (Metadata letöltése…)", - "checking_files": "{{title}} ({{percentage}} - Fájlok ellenőrzése…)", "paused": "{{title}} (Szünet)", "downloading": "{{title}} ({{percentage}} - Letöltés…)", "filter": "Könyvtár szűrése", @@ -30,7 +29,6 @@ "bottom_panel": { "no_downloads_in_progress": "Nincsenek folyamatban lévő letöltések", "downloading_metadata": "{{title}} metaadatainak letöltése…", - "checking_files": "{{title}} fájlok ellenőrzése… ({{percentage}} kész)", "downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}" }, "catalogue": { @@ -52,14 +50,12 @@ "space_left_on_disk": "{{space}} szabad hely a lemezen", "eta": "Befejezés {{eta}}", "downloading_metadata": "Metaadatok letöltése…", - "checking_files": "Fájlok ellenőrzése…", "filter": "Repackek szűrése", "requirements": "Rendszerkövetelmények", "minimum": "Minimális", "recommended": "Ajánlott", "no_minimum_requirements": "{{title}} nem tartalmaz információt a minimális követelményekről", "no_recommended_requirements": "{{title}} nem tartalmaz információt az ajánlott követelményekről", - "paused_progress": "{{progress}} (Szünetel)", "release_date": "Megjelenés: {{date}}", "publisher": "Kiadta: {{publisher}}", "copy_link_to_clipboard": "Link másolása", @@ -82,7 +78,6 @@ "playing_now": "Jelenleg játszva", "change": "Változtatás", "repacks_modal_description": "Choose the repack you want to download", - "downloads_path": "Letöltések helye", "select_folder_hint": "Ahhoz, hogy megváltoztasd a helyet, hozzákell férned a", "download_now": "Töltsd le most" }, @@ -102,13 +97,11 @@ "verifying": "Ellenőrzés…", "completed_at": "Befejezve {{date}}-kor", "completed": "Befejezve", - "cancelled": "Megszakítva", "download_again": "Újra letöltés", "cancel": "Mégse", "filter": "Letöltött játékok szűrése", "remove": "Eltávolítás", "downloading_metadata": "Metaadatok letöltése…", - "checking_files": "Fájlok ellenőrzése…", "starting_download": "Letöltés indítása…", "deleting": "Telepítő törlése…", "delete": "Telepítő eltávolítása", diff --git a/src/locales/id/translation.json b/src/locales/id/translation.json index 60de327a..f58c9757 100644 --- a/src/locales/id/translation.json +++ b/src/locales/id/translation.json @@ -12,7 +12,6 @@ "settings": "Pengaturan", "my_library": "Koleksi saya", "downloading_metadata": "{{title}} (Mengunduh metadata…)", - "checking_files": "{{title}} ({{percentage}} - Memeriksa file…)", "paused": "{{title}} (Terhenti)", "downloading": "{{title}} ({{percentage}} - Mengunduh…)", "filter": "Filter koleksi", @@ -34,7 +33,6 @@ "bottom_panel": { "no_downloads_in_progress": "Tidak ada unduhan berjalan", "downloading_metadata": "Mengunduh metadata {{title}}...", - "checking_files": "Memeriksa file {{title}}… ({{percentage}} selesai)", "downloading": "Mengunduh {{title}}… ({{percentage}} selesai) - Perkiraan {{eta}} - {{speed}}" }, "catalogue": { @@ -56,14 +54,12 @@ "space_left_on_disk": "{{space}} tersisa pada disk", "eta": "Perkiraan {{eta}}", "downloading_metadata": "Mengunduh metadata…", - "checking_files": "Memeriksa file…", "filter": "Saring repacks", "requirements": "Keperluan sistem", "minimum": "Minimum", "recommended": "Rekomendasi", "no_minimum_requirements": "{{title}} Tidak ada informasi kebutuhan sistem", "no_recommended_requirements": "{{title}} Tidak ada informasi rekomendasi kebutuhan sistem", - "paused_progress": "{{progress}} (Terhenti)", "release_date": "Dirilis pada {{date}}", "publisher": "Dipublikasikan oleh {{publisher}}", "copy_link_to_clipboard": "Salin tautan", @@ -86,7 +82,6 @@ "playing_now": "Memainkan sekarang", "change": "Ubah", "repacks_modal_description": "Pilih repack yang kamu ingin unduh", - "downloads_path": "Lokasi Unduhan", "select_folder_hint": "Untuk merubah folder bawaan, akses melalui", "download_now": "Unduh sekarang", "installation_instructions": "Instruksi Instalasi", @@ -114,13 +109,11 @@ "verifying": "Memeriksa…", "completed_at": "Selesai pada {{date}}", "completed": "Selesai", - "cancelled": "Dibatalkan", "download_again": "Unduh lagi", "cancel": "Batalkan", "filter": "Saring game yang diunduh", "remove": "Hapus", "downloading_metadata": "Mengunduh metadata…", - "checking_files": "Memeriksa file…", "starting_download": "Memulai unduhan…", "deleting": "Menghapus file instalasi…", "delete": "Hapus file instalasi", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index dc385156..5cb4e8a0 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -12,7 +12,6 @@ "settings": "Impostazioni", "my_library": "La mia libreria", "downloading_metadata": "{{title}} (Scaricamento metadati…)", - "checking_files": "{{title}} ({{percentage}} - Verifica file…)", "paused": "{{title}} (In pausa)", "downloading": "{{title}} ({{percentage}} - Download…)", "filter": "Filtra libreria", @@ -34,7 +33,6 @@ "bottom_panel": { "no_downloads_in_progress": "Nessun download in corso", "downloading_metadata": "Scaricamento metadati di {{title}}…", - "checking_files": "Verifica file di {{title}}… ({{percentage}} completato)", "downloading": "Download di {{title}}… ({{percentage}} completato) - Conclusione {{eta}} - {{speed}}" }, "catalogue": { @@ -56,14 +54,12 @@ "space_left_on_disk": "{{space}} rimasto sul disco", "eta": "Conclusione {{eta}}", "downloading_metadata": "Scaricamento metadati…", - "checking_files": "Verifica file…", "filter": "Filtra repack", "requirements": "Requisiti di sistema", "minimum": "Minimi", "recommended": "Consigliati", "no_minimum_requirements": "{{title}} non fornisce informazioni sui requisiti minimi", "no_recommended_requirements": "{{title}} non fornisce informazioni sui requisiti consigliati", - "paused_progress": "{{progress}} (In pausa)", "release_date": "Rilasciato il {{date}}", "publisher": "Pubblicato da {{publisher}}", "copy_link_to_clipboard": "Copia link", @@ -120,22 +116,18 @@ "verifying": "Verifica…", "completed_at": "Completato in {{date}}", "completed": "Completato", - "cancelled": "Annullato", "download_again": "Scarica di nuovo", "cancel": "Annulla", "filter": "Filtra giochi scaricati", "remove": "Rimuovi", "downloading_metadata": "Scaricamento metadati…", - "checking_files": "Verifica file…", "starting_download": "Avvio download…", "deleting": "Eliminazione dell'installer…", "delete": "Rimuovi installer", "remove_from_list": "Rimuovi", "delete_modal_title": "Sei sicuro?", "delete_modal_description": "Questo rimuoverà tutti i file di installazione dal tuo computer", - "install": "Installa", - "real_debrid": "Real Debrid", - "torrent": "Torrent" + "install": "Installa" }, "settings": { "downloads_path": "Percorso dei download", @@ -151,8 +143,7 @@ "general": "Generale", "behavior": "Comportamento", "enable_real_debrid": "Abilita Real Debrid", - "real_debrid": "Real Debrid", - "real_debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here.", + "real_debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here", "save_changes": "Salva modifiche" }, "notifications": { @@ -176,11 +167,5 @@ }, "modal": { "close": "Pulsante Chiudi" - }, - "splash": { - "downloading_version": "Scaricando la versione {{version}}", - "searching_updates": "Ricerca di aggiornamenti", - "update_found": "Trovato aggiornamento {{version}}", - "restarting_and_applying": "Riavvio e applico l'aggiornamento" } } diff --git a/src/locales/ko/translation.json b/src/locales/ko/translation.json index 94a945d1..748bc616 100644 --- a/src/locales/ko/translation.json +++ b/src/locales/ko/translation.json @@ -12,7 +12,6 @@ "settings": "설정", "my_library": "내 라이브러리", "downloading_metadata": "{{title}} (메타데이터 다운로드 중…)", - "checking_files": "{{title}} ({{percentage}} - 파일 검사 중…)", "paused": "{{title}} (일시 정지됨)", "downloading": "{{title}} ({{percentage}} - 다운로드 중…)", "filter": "라이브러리 정렬", @@ -34,7 +33,6 @@ "bottom_panel": { "no_downloads_in_progress": "진행중인 다운로드 없음", "downloading_metadata": "{{title}}의 메타데이터를 다운로드 중…", - "checking_files": "{{title}}의 파일들을 검사 중… ({{percentage}} 완료)", "downloading": "{{title}}의 파일들을 다운로드 중… ({{percentage}} 완료) - 완료까지 {{eta}} - {{speed}}" }, "catalogue": { @@ -56,14 +54,12 @@ "space_left_on_disk": "여유 저장 용량 {{space}} 남음", "eta": "완료까지 {{eta}}", "downloading_metadata": "메타데이터 다운로드 중…", - "checking_files": "파일 검사 중…", "filter": "리팩들을 다음과 같이 정렬하기", "requirements": "시스템 사양", "minimum": "최저 사양", "recommended": "권장 사양", "no_minimum_requirements": "{{title}}의 최저 사양을 제공받지 못 함", "no_recommended_requirements": "{{title}}의 권장 사양을 제공받지 못 함", - "paused_progress": "{{progress}} (일시 정지)", "release_date": "{{date}}에 발매됨", "publisher": "{{publisher}} 배급", "copy_link_to_clipboard": "링크 복사하기", @@ -86,7 +82,6 @@ "playing_now": "현재 플레이 중", "change": "바꾸기", "repacks_modal_description": "다운로드 할 리팩을 선택해 주세요", - "downloads_path": "다운로드 경로", "select_folder_hint": "기본 폴더를 바꾸려면 <0>설정으로 가세요", "download_now": "지금 다운로드", "installation_instructions": "설치 방법", @@ -114,22 +109,18 @@ "verifying": "검증중…", "completed_at": "{{date}}에 완료됨", "completed": "완료됨", - "cancelled": "취소됨", "download_again": "다시 다운로드 하기", "cancel": "취소", "filter": "다운로드 된 게임들을 정렬하기", "remove": "제거하기", "downloading_metadata": "메타데이터 다운로드 중…", - "checking_files": "파일 검사 중…", "starting_download": "다운로드 개시 중…", "deleting": "인스톨러 삭제 중…", "delete": "인스톨러 삭제하기", "remove_from_list": "제거하기", "delete_modal_title": "정말로 하시겠습니까?", "delete_modal_description": "이 기기의 모든 설치 파일들이 제거될 것입니다", - "install": "설치", - "real_debrid": "Real Debrid", - "torrent": "Torrent" + "install": "설치" }, "settings": { "downloads_path": "다운로드 경로", @@ -139,13 +130,11 @@ "enable_repack_list_notifications": "새 리팩이 추가되었을 때", "telemetry": "자동 데이터 수집", "telemetry_description": "익명 사용 통계를 활성화", - "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_api_token_hint": "API 키를 <0>이곳에서 얻으세요.", "save_changes": "변경 사항 저장" }, diff --git a/src/locales/nl/translation.json b/src/locales/nl/translation.json index 4be69007..fe48fdf4 100644 --- a/src/locales/nl/translation.json +++ b/src/locales/nl/translation.json @@ -12,7 +12,6 @@ "settings": "Instellingen", "my_library": "Mijn Bibliotheek", "downloading_metadata": "{{title}} (Downloading metadata…)", - "checking_files": "{{title}} ({{percentage}} - Folders checken…)", "paused": "{{title}} (Gepauzeerd)", "downloading": "{{title}} ({{percentage}} - Downloading…)", "filter": "Filter Bibliotheek", @@ -34,7 +33,6 @@ "bottom_panel": { "no_downloads_in_progress": "Geen Downloads bezig", "downloading_metadata": "Downloading {{title}} metadata…", - "checking_files": "Checking {{title}} files… ({{percentage}} complete)", "downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}" }, "catalogue": { @@ -56,14 +54,12 @@ "space_left_on_disk": "{{space}} Over op schijf", "eta": "Conclusie {{eta}}", "downloading_metadata": "Downloading metadata…", - "checking_files": "Files nakijken…", "filter": "Filter repacks", "requirements": "Systeem vereisten", "minimum": "Minimaal", "recommended": "Aanbevolen", "no_minimum_requirements": "{{title}} biedt geen informatie over de minimale vereisten", "no_recommended_requirements": "{{title}} biedt geen informatie over aanbevolen vereisten", - "paused_progress": "{{progress}} (Paused)", "release_date": "Uitgebracht op {{date}}", "publisher": "Gepubliceerd door {{publisher}}", "copy_link_to_clipboard": "Kopieer link", @@ -86,7 +82,6 @@ "playing_now": "Speel nu", "change": "Verander", "repacks_modal_description": "Kies de herverpakking die u wilt downloaden", - "downloads_path": "Downloads path", "select_folder_hint": "Om de standaardmap te wijzigen, gaat u naar <0>instellingen", "download_now": "Download nu", "installation_instructions": "Installatie instructies", @@ -114,22 +109,18 @@ "verifying": "Verifiëren…", "completed_at": "Voltooid binnen {{date}}", "completed": "Voltooid", - "cancelled": "Geannuleerd", "download_again": "Opnieuw downloaden", "cancel": "Annuleren", "filter": "Filter gedownloade games", "remove": "Verwijderen", "downloading_metadata": "Metagegevens downloaden", - "checking_files": "Bestanden controleren", "starting_download": "download starten", "deleting": "Installatieprogramma verwijderen…", "delete": "Installatieprogramma verwijderen", "remove_from_list": "Verwijderen", "delete_modal_title": "Weet je het zeker?", "delete_modal_description": "Hiermee worden alle installatiebestanden van uw computer verwijderd", - "install": "Installeren", - "real_debrid": "Real Debrid", - "torrent": "Torrent" + "install": "Installeren" }, "settings": { "downloads_path": "Downloadpad", @@ -139,13 +130,12 @@ "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_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 e6e106c2..b2ec4e4b 100644 --- a/src/locales/pl/translation.json +++ b/src/locales/pl/translation.json @@ -12,7 +12,6 @@ "settings": "Ustawienia", "my_library": "Moja biblioteka", "downloading_metadata": "{{title}} (Pobieranie metadata…)", - "checking_files": "{{title}} ({{percentage}} - Sprawdzanie plików…)", "paused": "{{title}} (Zatrzymano)", "downloading": "{{title}} ({{percentage}} - Pobieranie…)", "filter": "Filtruj biblioteke", @@ -34,7 +33,6 @@ "bottom_panel": { "no_downloads_in_progress": "Brak pobierań w toku", "downloading_metadata": "Pobieranie {{title}} metadata…", - "checking_files": "Sprawdzanie {{title}} plików… (ukończone w {{percentage}})", "downloading": "Pobieranie {{title}}… (ukończone w {{percentage}}) - Podsumowanie {{eta}} - {{speed}}" }, "catalogue": { @@ -56,14 +54,12 @@ "space_left_on_disk": "{{space}} wolnego na dysku", "eta": "Podsumowanie {{eta}}", "downloading_metadata": "Pobieranie metadata…", - "checking_files": "Sprawdzanie plików…", "filter": "Filtruj repacki", "requirements": "Wymagania systemowe", "minimum": "Minimalne", "recommended": "Zalecane", "no_minimum_requirements": "{{title}} nie zawiera informacji o minimalnych wymaganiach", "no_recommended_requirements": "{{title}} nie zawiera informacji o zalecanych wymaganiach", - "paused_progress": "{{progress}} (Zatrzymano)", "release_date": "Wydano w {{date}}", "publisher": "Opublikowany przez {{publisher}}", "copy_link_to_clipboard": "Kopiuj łącze", @@ -120,22 +116,18 @@ "verifying": "Weryfikowanie…", "completed_at": "Zakończono w {{date}}", "completed": "Zakończono", - "cancelled": "Anulowano", "download_again": "Pobierz ponownie", "cancel": "Anuluj", "filter": "Filtruj pobrane gry", "remove": "Usuń", "downloading_metadata": "Pobieranie metadata…", - "checking_files": "Sprawdzanie plików…", "starting_download": "Rozpoczęto pobieranie…", "deleting": "Usuwanie instalatora…", "delete": "Usuń instalator", "remove_from_list": "Usuń", "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", - "torrent": "Torrent" + "install": "Instaluj" }, "settings": { "downloads_path": "Ścieżka pobierania", @@ -145,16 +137,15 @@ "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", - "real_debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj.", - "save_changes": "Zapisz zmiany", - "language": "Język" + "language": "Język", + "enable_real_debrid": "Włącz Real-Debrid", + "real_debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj", + "save_changes": "Zapisz zmiany" }, "notifications": { "download_complete": "Pobieranie zakończone", diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index 8d3e0a76..df56c108 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -9,10 +9,9 @@ "sidebar": { "catalogue": "Catálogo", "downloads": "Downloads", - "settings": "Configurações", + "settings": "Ajustes", "my_library": "Minha biblioteca", "downloading_metadata": "{{title}} (Baixando metadados…)", - "checking_files": "{{title}} ({{percentage}} - Verificando arquivos…)", "paused": "{{title}} (Pausado)", "downloading": "{{title}} ({{percentage}} - Baixando…)", "filter": "Filtrar biblioteca", @@ -28,14 +27,15 @@ "catalogue": "Catálogo", "downloads": "Downloads", "search_results": "Resultados da busca", - "settings": "Configurações", - "home": "Início" + "settings": "Ajustes", + "home": "Início", + "version_available": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar." }, "bottom_panel": { "no_downloads_in_progress": "Sem downloads em andamento", "downloading_metadata": "Baixando metadados de {{title}}…", - "checking_files": "Verificando arquivos de {{title}}… ({{percentage}} completo)", - "downloading": "Baixando {{title}}… ({{percentage}} completo) - Conclusão {{eta}} - {{speed}}" + "downloading": "Baixando {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}", + "calculating_eta": "Baixando {{title}}… ({{percentage}} concluído) - Calculando tempo restante…" }, "game_details": { "open_download_options": "Ver opções de download", @@ -50,15 +50,15 @@ "remove_from_list": "Remover", "space_left_on_disk": "{{space}} livres em disco", "eta": "Conclusão {{eta}}", + "calculating_eta": "Calculando tempo restante…", "downloading_metadata": "Baixando metadados…", - "checking_files": "Verificando arquivos…", "filter": "Filtrar repacks", "requirements": "Requisitos do sistema", "minimum": "Mínimos", "recommended": "Recomendados", "no_minimum_requirements": "{{title}} não possui informações de requisitos mínimos", "no_recommended_requirements": "{{title}} não possui informações de requisitos recomendados", - "paused_progress": "{{progress}} (Pausado)", + "paused": "Pausado", "release_date": "Lançado em {{date}}", "publisher": "Publicado por {{publisher}}", "copy_link_to_clipboard": "Copiar link", @@ -82,8 +82,8 @@ "playing_now": "Jogando agora", "change": "Mudar", "repacks_modal_description": "Escolha o repack do jogo que deseja baixar", - "select_folder_hint": "Para trocar a pasta padrão, acesse a <0>Tela de Configurações", - "download_now": "Baixe agora", + "select_folder_hint": "Para trocar o diretório padrão, acesse a <0>Tela de Ajustes", + "download_now": "Iniciar download", "installation_instructions": "Instruções de Instalação", "installation_instructions_description": "Passos adicionais são necessários para instalar esse jogo", "online_fix_instruction": "Jogos OnlineFix precisam de uma senha para serem extraídos. Quando solicitado, utilize a seguinte senha:", @@ -98,7 +98,9 @@ "previous_screenshot": "Captura de tela anterior", "next_screenshot": "Próxima captura de tela", "screenshot": "Captura de tela {{number}}", - "open_screenshot": "Ver captura de tela {{number}}" + "open_screenshot": "Ver captura de tela {{number}}", + "download_settings": "Ajustes do download", + "downloader": "Downloader" }, "activation": { "title": "Ativação", @@ -116,22 +118,19 @@ "verifying": "Verificando…", "completed_at": "Concluído em {{date}}", "completed": "Concluído", - "cancelled": "Cancelado", + "removed": "Não baixado", "download_again": "Baixar novamente", "cancel": "Cancelar", "filter": "Filtrar jogos baixados", "remove": "Remover", "downloading_metadata": "Baixando metadados…", - "checking_files": "Verificando arquivos…", "starting_download": "Iniciando download…", "remove_from_list": "Remover", "delete": "Remover instalador", "delete_modal_description": "Isso removerá todos os arquivos de instalação do seu computador", "delete_modal_title": "Tem certeza?", "deleting": "Excluindo instalador…", - "install": "Instalar", - "torrent": "Torrent", - "real_debrid": "Real Debrid" + "install": "Instalar" }, "settings": { "downloads_path": "Diretório dos downloads", @@ -141,16 +140,21 @@ "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", - "quit_app_instead_hiding": "Fechar o aplicativo em vez de minimizá-lo", - "launch_with_system": "Iniciar aplicativo na inicialização do sistema", + "real_debrid_api_token_label": "Token de API do Real-Debrid", + "quit_app_instead_hiding": "Encerrar o Hydra ao invés de minimizá-lo ao fechar", + "launch_with_system": "Iniciar o Hydra junto com o sistema", "general": "Geral", "behavior": "Comportamento", - "enable_real_debrid": "Habilitar Real Debrid", - "real_debrid": "Real Debrid", - "real_debrid_api_token_hint": "Você pode obter sua chave de API <0>aqui.", + "language": "Idioma", + "real_debrid_api_token": "Token de API", + "enable_real_debrid": "Habilitar Real-Debrid", + "real_debrid_api_token_hint": "Você pode obter seu token de API <0>aqui", + "real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite baixar arquivos instantaneamente e com a melhor velocidade da sua Internet.", + "real_debrid_invalid_token": "Token de API inválido", + "real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, assine a Real-Debrid", + "real_debrid_linked_message": "Conta \"{{username}}\" vinculada", "save_changes": "Salvar mudanças", - "language": "Idioma" + "changes_saved": "Ajustes salvos com sucesso" }, "notifications": { "download_complete": "Download concluído", @@ -177,11 +181,5 @@ }, "modal": { "close": "Botão de fechar" - }, - "splash": { - "downloading_version": "Baixando versão {{version}}", - "searching_updates": "Buscando atualizações", - "update_found": "Versão {{version}} encontrada", - "restarting_and_applying": "Reiniciando e aplicando atualização" } } diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 6094cb21..2128ac79 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -12,7 +12,6 @@ "settings": "Настройки", "my_library": "Библиотека", "downloading_metadata": "{{title}} (Загрузка метаданных…)", - "checking_files": "{{title}} ({{percentage}} - Проверка файлов…)", "paused": "{{title}} (Приостановлено)", "downloading": "{{title}} ({{percentage}} - Загрузка…)", "filter": "Фильтр библиотеки", @@ -34,7 +33,6 @@ "bottom_panel": { "no_downloads_in_progress": "Нет активных загрузок", "downloading_metadata": "Загрузка метаданных {{title}}…", - "checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)", "downloading": "Загрузка {{title}}… ({{percentage}} завершено) - Окончание {{eta}} - {{speed}}" }, "catalogue": { @@ -56,14 +54,12 @@ "space_left_on_disk": "{{space}} свободно на диске", "eta": "Окончание {{eta}}", "downloading_metadata": "Загрузка метаданных…", - "checking_files": "Проверка файлов…", "filter": "Фильтр репаков", "requirements": "Системные требования", "minimum": "Минимальные", "recommended": "Рекомендуемые", "no_minimum_requirements": "Для {{title}} не указаны минимальные требования", "no_recommended_requirements": "Для {{title}} не указаны рекомендуемые требования", - "paused_progress": "{{progress}} (Приостановлено)", "release_date": "Выпущено {{date}}", "publisher": "Издатель {{publisher}}", "copy_link_to_clipboard": "Копировать ссылку", @@ -120,22 +116,18 @@ "verifying": "Проверка…", "completed_at": "Завершено в {{date}}", "completed": "Завершено", - "cancelled": "Отменено", "download_again": "Загрузить снова", "cancel": "Отменить", "filter": "Фильтр загруженных игр", "remove": "Удалить", "downloading_metadata": "Загрузка метаданных…", - "checking_files": "Проверка файлов…", "starting_download": "Начало загрузки…", "deleting": "Удаление установщика…", "delete": "Удалить установщик", "remove_from_list": "Удалить", "delete_modal_title": "Вы уверены?", "delete_modal_description": "Это удалит все установщики с вашего компьютера", - "install": "Установить", - "real_debrid": "Real Debrid", - "torrent": "Torrent" + "install": "Установить" }, "settings": { "downloads_path": "Путь загрузок", @@ -145,14 +137,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", - "real_debrid_api_token_hint": "API ключ можно получить <0>здесь.", + "enable_real_debrid": "Включить Real-Debrid", + "real_debrid_api_token_hint": "API ключ можно получить <0>здесь", "save_changes": "Сохранить изменения" }, "notifications": { @@ -176,11 +167,5 @@ }, "modal": { "close": "Закрыть" - }, - "splash": { - "downloading_version": "Загрузка версии {{version}}", - "searching_updates": "Поиск обновлений", - "update_found": "Найдена новая версия {{version}}", - "restarting_and_applying": "Перезапуск и внесение изменений" } } diff --git a/src/locales/tr/translation.json b/src/locales/tr/translation.json index be40e013..1e6ef65f 100644 --- a/src/locales/tr/translation.json +++ b/src/locales/tr/translation.json @@ -12,7 +12,6 @@ "settings": "Ayarlar", "my_library": "Kütüphane", "downloading_metadata": "{{title}} (Metadata indiriliyor…)", - "checking_files": "{{title}} ({{percentage}} - Dosyalar kontrol ediliyor…)", "paused": "{{title}} (Duraklatıldı)", "downloading": "{{title}} ({{percentage}} - İndiriliyor…)", "filter": "Kütüphaneyi filtrele", @@ -34,7 +33,6 @@ "bottom_panel": { "no_downloads_in_progress": "İndirilen bir şey yok", "downloading_metadata": "{{title}} metadatası indiriliyor…", - "checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)", "downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Bitiş {{eta}} - {{speed}}" }, "catalogue": { @@ -56,14 +54,12 @@ "space_left_on_disk": "Diskte {{space}} yer kaldı", "eta": "Bitiş {{eta}}", "downloading_metadata": "Metadata indiriliyor…", - "checking_files": "Dosyalar kontrol ediliyor…", "filter": "Repackleri filtrele", "requirements": "Sistem gereksinimleri", "minimum": "Minimum", "recommended": "Önerilen", "no_minimum_requirements": "{{title}} minimum sistem gereksinim bilgilerini karşılamıyor", "no_recommended_requirements": "{{title}} önerilen sistem gereksinim bilgilerini karşılamıyor", - "paused_progress": "{{progress}} (Duraklatıldı)", "release_date": "{{date}} tarihinde çıktı", "publisher": "{{publisher}} tarihinde yayınlandı", "copy_link_to_clipboard": "Link'i kopyala", @@ -86,7 +82,6 @@ "playing_now": "Şimdi oynanıyor", "change": "Değiştir", "repacks_modal_description": "İndirmek istediğiiniz repacki seçin", - "downloads_path": "İndirme yolu", "select_folder_hint": "Varsayılan klasörü değiştirmek için ulaşmanız gereken ayar", "download_now": "Şimdi", "installation_instructions": "Kurulum", @@ -114,13 +109,11 @@ "verifying": "Doğrulanıyor…", "completed_at": "{{date}} tarihinde tamamlanacak", "completed": "Tamamlandı", - "cancelled": "İptal edildi", "download_again": "Tekrar indir", "cancel": "İptal et", "filter": "Yüklü oyunları filtrele", "remove": "Kaldır", "downloading_metadata": "Metadata indiriliyor…", - "checking_files": "Dosyalar kontrol ediliyor…", "starting_download": "İndirme başlatılıyor…", "deleting": "Installer siliniyor…", "delete": "Installer'ı sil", diff --git a/src/locales/uk/translation.json b/src/locales/uk/translation.json index e0f6af7b..ad11f777 100644 --- a/src/locales/uk/translation.json +++ b/src/locales/uk/translation.json @@ -12,7 +12,6 @@ "settings": "Налаштування", "my_library": "Бібліотека", "downloading_metadata": "{{title}} (Завантаження метаданих…)", - "checking_files": "{{title}} ({{percentage}} - Перевірка файлів…)", "paused": "{{title}} (Призупинено)", "downloading": "{{title}} ({{percentage}} - Завантаження…)", "filter": "Фільтр бібліотеки", @@ -34,7 +33,6 @@ "bottom_panel": { "no_downloads_in_progress": "Немає активних завантажень", "downloading_metadata": "Завантаження метаданих {{title}}…", - "checking_files": "Перевірка файлів {{title}}… ({{percentage}} завершено)", "downloading": "Завантаження {{title}}… ({{percentage}} завершено) - Закінчення {{eta}} - {{speed}}" }, "catalogue": { @@ -56,14 +54,12 @@ "space_left_on_disk": "{{space}} вільно на диску", "eta": "Закінчення {{eta}}", "downloading_metadata": "Завантаження метаданих…", - "checking_files": "Перевірка файлів…", "filter": "Фільтр репаків", "requirements": "Системні вимоги", "minimum": "Мінімальні", "recommended": "Рекомендовані", "no_minimum_requirements": "Для {{title}} не вказані мінімальні вимоги", "no_recommended_requirements": "Для {{title}} не вказані рекомендовані вимоги", - "paused_progress": "{{progress}} (Призупинено)", "release_date": "Випущено {{date}}", "publisher": "Видавець {{publisher}}", "copy_link_to_clipboard": "Скопіювати посилання", @@ -86,7 +82,6 @@ "playing_now": "Поточна гра", "change": "Змінити", "repacks_modal_description": "Виберіть репак, який хочете завантажити", - "downloads_path": "Шлях завантажень", "select_folder_hint": "Щоб змінити теку за замовчуванням, відкрийте", "download_now": "Завантажити зараз", "installation_instructions": "Інструкція зі встановлення", @@ -114,13 +109,11 @@ "verifying": "Перевірка…", "completed_at": "Завершено в {{date}}", "completed": "Завершено", - "cancelled": "Скасовано", "download_again": "Завантажити знову", "cancel": "Скасувати", "filter": "Фільтр завантажених ігор", "remove": "Видалити", "downloading_metadata": "Завантаження метаданих…", - "checking_files": "Перевірка файлів…", "starting_download": "Початок завантаження…", "deleting": "Видалення інсталятора…", "delete": "Видалити інсталятор", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 8481362a..e0d71b58 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -12,7 +12,6 @@ "settings": "设置", "my_library": "我的游戏库", "downloading_metadata": "{{title}} (正在下载元数据…)", - "checking_files": "{{title}} ({{percentage}} - 正在检查文件…)", "paused": "{{title}} (已暂停)", "downloading": "{{title}} ({{percentage}} - 正在下载…)", "filter": "筛选游戏库", @@ -34,7 +33,6 @@ "bottom_panel": { "no_downloads_in_progress": "没有正在进行的下载", "downloading_metadata": "正在下载{{title}}的元数据…", - "checking_files": "正在检查{{title}}的文件… ({{percentage}}完成)", "downloading": "正在下载{{title}}… ({{percentage}}完成) - 剩余时间{{eta}} - 速度{{speed}}" }, "catalogue": { @@ -56,14 +54,12 @@ "space_left_on_disk": "磁盘剩余空间{{space}}", "eta": "预计完成时间{{eta}}", "downloading_metadata": "正在下载元数据…", - "checking_files": "正在检查文件…", "filter": "筛选重打包", "requirements": "配置要求", "minimum": "最低要求", "recommended": "推荐要求", "no_minimum_requirements": "{{title}}没有提供最低要求信息", "no_recommended_requirements": "{{title}}没有提供推荐要求信息", - "paused_progress": "{{progress}} (已暂停)", "release_date": "发布于{{date}}", "publisher": "发行商{{publisher}}", "copy_link_to_clipboard": "复制链接", @@ -86,9 +82,7 @@ "playing_now": "正在游戏中", "change": "更改", "repacks_modal_description": "选择您想要下载的重打包", - "downloads_path": "下载路径", "select_folder_hint": "要更改默认文件夹,请访问", - "settings": "设置", "download_now": "立即下载", "installation_instructions": "安装说明", "installation_instructions_description": "安装这个游戏需要额外的步骤", @@ -118,22 +112,18 @@ "verifying": "正在验证…", "completed_at": "完成于{{date}}", "completed": "已完成", - "cancelled": "已取消", "download_again": "再次下载", "cancel": "取消", "filter": "筛选已下载游戏", "remove": "移除", "downloading_metadata": "正在下载元数据…", - "checking_files": "正在检查文件…", "starting_download": "开始下载…", "deleting": "正在删除安装程序…", "delete": "移除安装程序", "remove_from_list": "移除", "delete_modal_title": "您确定吗?", "delete_modal_description": "这将从您的电脑上移除所有的安装文件", - "install": "安装", - "real_debrid": "Real Debrid", - "torrent": "种子" + "install": "安装" }, "settings": { "downloads_path": "下载路径", @@ -143,13 +133,11 @@ "enable_repack_list_notifications": "添加新重打包时", "telemetry": "遥测", "telemetry_description": "启用匿名使用统计", - "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_api_token_hint": "您可以从<0>这里获取API密钥.", "save_changes": "保存更改" }, @@ -174,11 +162,5 @@ }, "modal": { "close": "关闭按钮" - }, - "splash": { - "downloading_version": "正在下载新版本 {{version}}", - "searching_updates": "检查更新...", - "update_found": "有新版本 {{version}} 可用", - "restarting_and_applying": "重启并应用更新" } } diff --git a/src/main/declaration.d.ts b/src/main/declaration.d.ts new file mode 100644 index 00000000..ac2675a3 --- /dev/null +++ b/src/main/declaration.d.ts @@ -0,0 +1,80 @@ +declare module "aria2" { + export type Aria2Status = + | "active" + | "waiting" + | "paused" + | "error" + | "complete" + | "removed"; + + export interface StatusResponse { + gid: string; + status: Aria2Status; + totalLength: string; + completedLength: string; + uploadLength: string; + bitfield: string; + downloadSpeed: string; + uploadSpeed: string; + infoHash?: string; + numSeeders?: string; + seeder?: boolean; + pieceLength: string; + numPieces: string; + connections: string; + errorCode?: string; + errorMessage?: string; + followedBy?: string[]; + following: string; + belongsTo: string; + dir: string; + files: { + path: string; + length: string; + completedLength: string; + selected: string; + }[]; + bittorrent?: { + announceList: string[][]; + comment: string; + creationDate: string; + mode: "single" | "multi"; + info: { + name: string; + verifiedLength: string; + verifyIntegrityPending: string; + }; + }; + } + + export default class Aria2 { + constructor(options: any); + open: () => Promise; + call( + method: "addUri", + uris: string[], + options: { dir: string } + ): Promise; + call( + method: "tellStatus", + gid: string, + keys?: string[] + ): Promise; + call(method: "pause", gid: string): Promise; + call(method: "forcePause", gid: string): Promise; + call(method: "unpause", gid: string): Promise; + call(method: "remove", gid: string): Promise; + call(method: "forceRemove", gid: string): Promise; + call(method: "pauseAll"): Promise; + call(method: "forcePauseAll"): Promise; + listNotifications: () => [ + "onDownloadStart", + "onDownloadPause", + "onDownloadStop", + "onDownloadComplete", + "onDownloadError", + "onBtDownloadComplete", + ]; + on: (event: string, callback: (params: any) => void) => void; + } +} diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 91e19ea6..784380e0 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -10,7 +10,8 @@ import { import { Repack } from "./repack.entity"; import type { GameShop } from "@types"; -import { Downloader, GameStatus } from "@shared"; +import { Downloader } from "@shared"; +import type { Aria2Status } from "aria2"; @Entity("game") export class Game { @@ -42,7 +43,7 @@ export class Game { shop: GameShop; @Column("text", { nullable: true }) - status: GameStatus | null; + status: Aria2Status | null; @Column("int", { default: Downloader.Torrent }) downloader: Downloader; @@ -53,13 +54,10 @@ export class Game { @Column("float", { default: 0 }) progress: number; - @Column("float", { default: 0 }) - fileVerificationProgress: number; - @Column("int", { default: 0 }) bytesDownloaded: number; - @Column("text", { nullable: true }) + @Column("datetime", { nullable: true }) lastTimePlayed: Date | null; @Column("float", { default: 0 }) diff --git a/src/main/events/autoupdater/check-for-updates.ts b/src/main/events/autoupdater/check-for-updates.ts index aa63575f..a5d6e87b 100644 --- a/src/main/events/autoupdater/check-for-updates.ts +++ b/src/main/events/autoupdater/check-for-updates.ts @@ -1,47 +1,32 @@ import { AppUpdaterEvents } from "@types"; import { registerEvent } from "../register-event"; -import updater, { ProgressInfo, UpdateInfo } from "electron-updater"; +import updater, { UpdateInfo } from "electron-updater"; import { WindowManager } from "@main/services"; import { app } from "electron"; const { autoUpdater } = updater; const sendEvent = (event: AppUpdaterEvents) => { - WindowManager.splashWindow?.webContents.send("autoUpdaterEvent", event); + WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event); }; -const mockValuesForDebug = async () => { - sendEvent({ type: "update-downloaded" }); +const mockValuesForDebug = () => { + sendEvent({ type: "update-available", info: { version: "1.3.0" } }); }; const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => { autoUpdater - .addListener("error", () => { - sendEvent({ type: "error" }); - }) - .addListener("checking-for-update", () => { - sendEvent({ type: "checking-for-updates" }); - }) - .addListener("update-not-available", () => { - sendEvent({ type: "update-not-available" }); - }) - .addListener("update-available", (info: UpdateInfo) => { + .once("update-available", (info: UpdateInfo) => { sendEvent({ type: "update-available", info }); }) - .addListener("update-downloaded", () => { + .once("update-downloaded", () => { sendEvent({ type: "update-downloaded" }); - }) - .addListener("download-progress", (info: ProgressInfo) => { - sendEvent({ type: "download-progress", info }); - }) - .addListener("update-cancelled", () => { - sendEvent({ type: "update-cancelled" }); }); if (app.isPackaged) { autoUpdater.checkForUpdates(); } else { - await mockValuesForDebug(); + mockValuesForDebug(); } }; diff --git a/src/main/events/autoupdater/continue-to-main-window.ts b/src/main/events/autoupdater/continue-to-main-window.ts deleted file mode 100644 index 6a8965f9..00000000 --- a/src/main/events/autoupdater/continue-to-main-window.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { WindowManager } from "@main/services"; -import { registerEvent } from "../register-event"; -import updater from "electron-updater"; - -const { autoUpdater } = updater; - -const continueToMainWindow = async (_event: Electron.IpcMainInvokeEvent) => { - autoUpdater.removeAllListeners(); - WindowManager.prepareMainWindowAndCloseSplash(); -}; - -registerEvent("continueToMainWindow", continueToMainWindow); diff --git a/src/main/events/autoupdater/restart-and-install-update.ts b/src/main/events/autoupdater/restart-and-install-update.ts index be301c18..2dbef98f 100644 --- a/src/main/events/autoupdater/restart-and-install-update.ts +++ b/src/main/events/autoupdater/restart-and-install-update.ts @@ -1,16 +1,13 @@ import { app } from "electron"; import { registerEvent } from "../register-event"; import updater from "electron-updater"; -import { WindowManager } from "@main/services"; const { autoUpdater } = updater; const restartAndInstallUpdate = async (_event: Electron.IpcMainInvokeEvent) => { + autoUpdater.removeAllListeners(); if (app.isPackaged) { autoUpdater.quitAndInstall(true, true); - } else { - autoUpdater.removeAllListeners(); - WindowManager.prepareMainWindowAndCloseSplash(); } }; diff --git a/src/main/events/index.ts b/src/main/events/index.ts index debca0e4..0ebb5d86 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -29,7 +29,7 @@ import "./user-preferences/update-user-preferences"; import "./user-preferences/auto-launch"; import "./autoupdater/check-for-updates"; import "./autoupdater/restart-and-install-update"; -import "./autoupdater/continue-to-main-window"; +import "./user-preferences/authenticate-real-debrid"; ipcMain.handle("ping", () => "pong"); ipcMain.handle("getVersion", () => app.getVersion()); diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 35ffa65a..1db812fa 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -10,7 +10,7 @@ const addGameToLibrary = async ( _event: Electron.IpcMainInvokeEvent, objectID: string, title: string, - gameShop: GameShop, + shop: GameShop, executablePath: string | null ) => { return gameRepository @@ -19,7 +19,7 @@ const addGameToLibrary = async ( objectID, }, { - shop: gameShop, + shop, status: null, executablePath, isDeleted: false, @@ -40,7 +40,7 @@ const addGameToLibrary = async ( title, iconUrl, objectID, - shop: gameShop, + shop, executablePath, }) .then(() => { diff --git a/src/main/events/library/delete-game-folder.ts b/src/main/events/library/delete-game-folder.ts index 954367a0..1e98e6fe 100644 --- a/src/main/events/library/delete-game-folder.ts +++ b/src/main/events/library/delete-game-folder.ts @@ -1,7 +1,8 @@ import path from "node:path"; import fs from "node:fs"; -import { GameStatus } from "@shared"; +import { In } from "typeorm"; + import { gameRepository } from "@main/repository"; import { getDownloadsPath } from "../helpers/get-downloads-path"; @@ -15,7 +16,7 @@ const deleteGameFolder = async ( const game = await gameRepository.findOne({ where: { id: gameId, - status: GameStatus.Cancelled, + status: In(["removed", "complete"]), isDeleted: false, }, }); diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index 2374c497..4fd4e254 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -2,7 +2,6 @@ import { gameRepository } from "@main/repository"; import { searchRepacks } from "../helpers/search-games"; import { registerEvent } from "../register-event"; -import { GameStatus } from "@shared"; import { sortBy } from "lodash-es"; const getLibrary = async () => @@ -24,7 +23,7 @@ const getLibrary = async () => ...game, repacks: searchRepacks(game.title), })), - (game) => (game.status !== GameStatus.Cancelled ? 0 : 1) + (game) => (game.status !== "removed" ? 0 : 1) ) ); diff --git a/src/main/events/library/remove-game.ts b/src/main/events/library/remove-game.ts index 57b10b37..687366c5 100644 --- a/src/main/events/library/remove-game.ts +++ b/src/main/events/library/remove-game.ts @@ -1,6 +1,5 @@ import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; -import { GameStatus } from "@shared"; const removeGame = async ( _event: Electron.IpcMainInvokeEvent, @@ -9,10 +8,9 @@ const removeGame = async ( await gameRepository.update( { id: gameId, - status: GameStatus.Cancelled, }, { - status: null, + status: "removed", downloadPath: null, bytesDownloaded: 0, progress: 0, diff --git a/src/main/events/torrenting/cancel-game-download.ts b/src/main/events/torrenting/cancel-game-download.ts index 18d29fde..3c9a0715 100644 --- a/src/main/events/torrenting/cancel-game-download.ts +++ b/src/main/events/torrenting/cancel-game-download.ts @@ -1,53 +1,25 @@ import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; -import { WindowManager } from "@main/services"; -import { In } from "typeorm"; import { DownloadManager } from "@main/services"; -import { GameStatus } from "@shared"; const cancelGameDownload = async ( _event: Electron.IpcMainInvokeEvent, gameId: number ) => { - const game = await gameRepository.findOne({ - where: { + await DownloadManager.cancelDownload(gameId); + + await gameRepository.update( + { id: gameId, - isDeleted: false, - status: In([ - GameStatus.Downloading, - GameStatus.DownloadingMetadata, - GameStatus.CheckingFiles, - GameStatus.Paused, - GameStatus.Seeding, - GameStatus.Finished, - ]), }, - }); - - if (!game) return; - DownloadManager.cancelDownload(); - - await gameRepository - .update( - { - id: game.id, - }, - { - status: GameStatus.Cancelled, - bytesDownloaded: 0, - progress: 0, - } - ) - .then((result) => { - if ( - game.status !== GameStatus.Paused && - game.status !== GameStatus.Seeding - ) { - if (result.affected) WindowManager.mainWindow?.setProgressBar(-1); - } - }); + { + status: "removed", + bytesDownloaded: 0, + progress: 0, + } + ); }; registerEvent("cancelGameDownload", cancelGameDownload); diff --git a/src/main/events/torrenting/pause-game-download.ts b/src/main/events/torrenting/pause-game-download.ts index ceda70cc..f9ed1102 100644 --- a/src/main/events/torrenting/pause-game-download.ts +++ b/src/main/events/torrenting/pause-game-download.ts @@ -1,30 +1,13 @@ import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; -import { In } from "typeorm"; -import { DownloadManager, WindowManager } from "@main/services"; -import { GameStatus } from "@shared"; +import { DownloadManager } from "@main/services"; const pauseGameDownload = async ( _event: Electron.IpcMainInvokeEvent, gameId: number ) => { - DownloadManager.pauseDownload(); - - await gameRepository - .update( - { - id: gameId, - status: In([ - GameStatus.Downloading, - GameStatus.DownloadingMetadata, - GameStatus.CheckingFiles, - ]), - }, - { status: GameStatus.Paused } - ) - .then((result) => { - if (result.affected) WindowManager.mainWindow?.setProgressBar(-1); - }); + await DownloadManager.pauseDownload(); + await gameRepository.update({ id: gameId }, { status: "paused" }); }; registerEvent("pauseGameDownload", pauseGameDownload); diff --git a/src/main/events/torrenting/resume-game-download.ts b/src/main/events/torrenting/resume-game-download.ts index 6982d895..f8007206 100644 --- a/src/main/events/torrenting/resume-game-download.ts +++ b/src/main/events/torrenting/resume-game-download.ts @@ -1,9 +1,11 @@ +import { Not } from "typeorm"; + import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; -import { getDownloadsPath } from "../helpers/get-downloads-path"; -import { In } from "typeorm"; + import { DownloadManager } from "@main/services"; -import { GameStatus } from "@shared"; +import { dataSource } from "@main/data-source"; +import { Game } from "@main/entity"; const resumeGameDownload = async ( _event: Electron.IpcMainInvokeEvent, @@ -18,31 +20,21 @@ const resumeGameDownload = async ( }); if (!game) return; - DownloadManager.pauseDownload(); - if (game.status === GameStatus.Paused) { - const downloadsPath = game.downloadPath ?? (await getDownloadsPath()); + if (game.status === "paused") { + await dataSource.transaction(async (transactionalEntityManager) => { + await DownloadManager.pauseDownload(); - DownloadManager.resumeDownload(gameId); + await transactionalEntityManager + .getRepository(Game) + .update({ status: "active", progress: Not(1) }, { status: "paused" }); - await gameRepository.update( - { - status: In([ - GameStatus.Downloading, - GameStatus.DownloadingMetadata, - GameStatus.CheckingFiles, - ]), - }, - { status: GameStatus.Paused } - ); + await DownloadManager.resumeDownload(game); - await gameRepository.update( - { id: game.id }, - { - status: GameStatus.Downloading, - downloadPath: downloadsPath, - } - ); + await transactionalEntityManager + .getRepository(Game) + .update({ id: gameId }, { status: "active" }); + }); } }; diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index f94d0999..c52ccb35 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -1,39 +1,26 @@ -import { - gameRepository, - repackRepository, - userPreferencesRepository, -} from "@main/repository"; +import { gameRepository, repackRepository } from "@main/repository"; import { registerEvent } from "../register-event"; -import type { GameShop } from "@types"; +import type { StartGameDownloadPayload } from "@types"; import { getFileBase64, getSteamAppAsset } from "@main/helpers"; -import { In } from "typeorm"; import { DownloadManager } from "@main/services"; -import { Downloader, GameStatus } from "@shared"; import { stateManager } from "@main/state-manager"; +import { Not } from "typeorm"; const startGameDownload = async ( _event: Electron.IpcMainInvokeEvent, - repackId: number, - objectID: string, - title: string, - gameShop: GameShop, - downloadPath: string + payload: StartGameDownloadPayload ) => { - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); - - const downloader = userPreferences?.realDebridApiToken - ? Downloader.RealDebrid - : Downloader.Torrent; + const { repackId, objectID, title, shop, downloadPath, downloader } = payload; const [game, repack] = await Promise.all([ gameRepository.findOne({ where: { objectID, + shop, }, + relations: { repack: true }, }), repackRepository.findOne({ where: { @@ -42,18 +29,13 @@ const startGameDownload = async ( }), ]); - if (!repack || game?.status === GameStatus.Downloading) return; - DownloadManager.pauseDownload(); + if (!repack) return; + + await DownloadManager.pauseDownload(); await gameRepository.update( - { - status: In([ - GameStatus.Downloading, - GameStatus.DownloadingMetadata, - GameStatus.CheckingFiles, - ]), - }, - { status: GameStatus.Paused } + { status: "active", progress: Not(1) }, + { status: "paused" } ); if (game) { @@ -62,19 +44,15 @@ const startGameDownload = async ( id: game.id, }, { - status: GameStatus.DownloadingMetadata, - downloadPath: downloadPath, + status: "active", + progress: 0, + bytesDownloaded: 0, + downloadPath, downloader, repack: { id: repackId }, isDeleted: false, } ); - - DownloadManager.downloadGame(game.id); - - game.status = GameStatus.DownloadingMetadata; - - return game; } else { const steamGame = stateManager .getValue("steamGames") @@ -84,14 +62,14 @@ const startGameDownload = async ( ? getSteamAppAsset("icon", objectID, steamGame.clientIcon) : null; - const createdGame = await gameRepository - .save({ + await gameRepository + .insert({ title, iconUrl, objectID, downloader, - shop: gameShop, - status: GameStatus.Downloading, + shop, + status: "active", downloadPath, repack: { id: repackId }, }) @@ -104,13 +82,16 @@ const startGameDownload = async ( return result; }); - - DownloadManager.downloadGame(createdGame.id); - - const { repack: _, ...rest } = createdGame; - - return rest; } + + const updatedGame = await gameRepository.findOne({ + where: { + objectID, + }, + relations: { repack: true }, + }); + + await DownloadManager.startDownload(updatedGame!); }; registerEvent("startGameDownload", startGameDownload); diff --git a/src/main/events/user-preferences/authenticate-real-debrid.ts b/src/main/events/user-preferences/authenticate-real-debrid.ts new file mode 100644 index 00000000..01705db7 --- /dev/null +++ b/src/main/events/user-preferences/authenticate-real-debrid.ts @@ -0,0 +1,14 @@ +import { RealDebridClient } from "@main/services/real-debrid"; +import { registerEvent } from "../register-event"; + +const authenticateRealDebrid = async ( + _event: Electron.IpcMainInvokeEvent, + apiToken: string +) => { + RealDebridClient.authorize(apiToken); + + const user = await RealDebridClient.getUser(); + return user; +}; + +registerEvent("authenticateRealDebrid", authenticateRealDebrid); 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/helpers/index.ts b/src/main/helpers/index.ts index 22a95ddc..367ff0d8 100644 --- a/src/main/helpers/index.ts +++ b/src/main/helpers/index.ts @@ -85,5 +85,8 @@ export const steamUrlBuilder = { `https://cdn.cloudflare.steamstatic.com/steam/apps/${objectID}/logo.png`, }; +export const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + export * from "./formatters"; export * from "./ps"; diff --git a/src/main/index.ts b/src/main/index.ts index 22c13388..74481090 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,7 +3,12 @@ import updater from "electron-updater"; import i18n from "i18next"; import path from "node:path"; import { electronApp, optimizer } from "@electron-toolkit/utils"; -import { logger, resolveDatabaseUpdates, WindowManager } from "@main/services"; +import { + DownloadManager, + logger, + resolveDatabaseUpdates, + WindowManager, +} from "@main/services"; import { dataSource } from "@main/data-source"; import * as resources from "@locales"; import { userPreferencesRepository } from "@main/repository"; @@ -64,7 +69,7 @@ app.whenReady().then(() => { where: { id: 1 }, }); - WindowManager.createSplashScreen(); + WindowManager.createMainWindow(); WindowManager.createSystemTray(userPreferences?.language || "en"); }); }); @@ -100,6 +105,10 @@ app.on("window-all-closed", () => { WindowManager.mainWindow = null; }); +app.on("before-quit", () => { + DownloadManager.disconnect(); +}); + app.on("activate", () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. diff --git a/src/main/main.ts b/src/main/main.ts index e03a6ab8..501f03fa 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -5,27 +5,25 @@ import { getNewRepacksFromUser, getNewRepacksFromXatab, getNewRepacksFromOnlineFix, - startProcessWatcher, DownloadManager, + startMainLoop, } from "./services"; import { gameRepository, repackRepository, userPreferencesRepository, } from "./repository"; -import { TorrentDownloader } from "./services"; import { Repack, UserPreferences } from "./entity"; import { Notification } from "electron"; import { t } from "i18next"; -import { GameStatus } from "@shared"; -import { In } from "typeorm"; import fs from "node:fs"; import path from "node:path"; import { RealDebridClient } from "./services/real-debrid"; import { orderBy } from "lodash-es"; import { SteamGame } from "@types"; +import { Not } from "typeorm"; -startProcessWatcher(); +startMainLoop(); const track1337xUsers = async (existingRepacks: Repack[]) => { for (const repacker of repackersOn1337x) { @@ -72,7 +70,7 @@ const checkForNewRepacks = async (userPreferences: UserPreferences | null) => { }; const loadState = async (userPreferences: UserPreferences | null) => { - const repacks = await repackRepository.find({ + const repacks = repackRepository.find({ order: { createdAt: "desc", }, @@ -82,31 +80,24 @@ const loadState = async (userPreferences: UserPreferences | null) => { fs.readFileSync(path.join(seedsPath, "steam-games.json"), "utf-8") ) as SteamGame[]; - stateManager.setValue("repacks", repacks); + stateManager.setValue("repacks", await repacks); stateManager.setValue("steamGames", orderBy(steamGames, ["name"], "asc")); import("./events"); if (userPreferences?.realDebridApiToken) - await RealDebridClient.authorize(userPreferences?.realDebridApiToken); + RealDebridClient.authorize(userPreferences?.realDebridApiToken); const game = await gameRepository.findOne({ where: { - status: In([ - GameStatus.Downloading, - GameStatus.DownloadingMetadata, - GameStatus.CheckingFiles, - ]), + status: "active", + progress: Not(1), isDeleted: false, }, relations: { repack: true }, }); - await TorrentDownloader.startClient(); - - if (game) { - DownloadManager.resumeDownload(game.id); - } + if (game) DownloadManager.startDownload(game); }; userPreferencesRepository diff --git a/src/main/migrations/1716776027208-alter_lastTimePlayed_to_datime.ts b/src/main/migrations/1716776027208-alter_lastTimePlayed_to_datime.ts new file mode 100644 index 00000000..6a562915 --- /dev/null +++ b/src/main/migrations/1716776027208-alter_lastTimePlayed_to_datime.ts @@ -0,0 +1,49 @@ +import { Game } from "@main/entity"; +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AlterLastTimePlayedToDatime1716776027208 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + // 2024-05-27 02:08:17 + // Mon, 27 May 2024 02:08:17 GMT + const updateLastTimePlayedValues = ` + UPDATE game SET lastTimePlayed = (SELECT + SUBSTR(lastTimePlayed, 13, 4) || '-' || -- Year + CASE SUBSTR(lastTimePlayed, 9, 3) + WHEN 'Jan' THEN '01' + WHEN 'Feb' THEN '02' + WHEN 'Mar' THEN '03' + WHEN 'Apr' THEN '04' + WHEN 'May' THEN '05' + WHEN 'Jun' THEN '06' + WHEN 'Jul' THEN '07' + WHEN 'Aug' THEN '08' + WHEN 'Sep' THEN '09' + WHEN 'Oct' THEN '10' + WHEN 'Nov' THEN '11' + WHEN 'Dec' THEN '12' + END || '-' || -- Month + SUBSTR(lastTimePlayed, 6, 2) || ' ' || -- Day + SUBSTR(lastTimePlayed, 18, 8) -- hh:mm:ss; + FROM game) + WHERE lastTimePlayed IS NOT NULL; + `; + + await queryRunner.query(updateLastTimePlayedValues); + } + + public async down(queryRunner: QueryRunner): Promise { + const queryBuilder = queryRunner.manager.createQueryBuilder(Game, "game"); + + const result = await queryBuilder.getMany(); + + for (const game of result) { + if (!game.lastTimePlayed) continue; + await queryRunner.query( + `UPDATE game set lastTimePlayed = ? WHERE id = ?;`, + [game.lastTimePlayed.toUTCString(), game.id] + ); + } + } +} diff --git a/src/main/migrations/index.ts b/src/main/migrations/index.ts index 65061fac..c0c96e45 100644 --- a/src/main/migrations/index.ts +++ b/src/main/migrations/index.ts @@ -1,3 +1,7 @@ import { FixRepackUploadDate1715900413313 } from "./1715900413313-fix_repack_uploadDate"; +import { AlterLastTimePlayedToDatime1716776027208 } from "./1716776027208-alter_lastTimePlayed_to_datime"; -export default [FixRepackUploadDate1715900413313]; +export default [ + FixRepackUploadDate1715900413313, + AlterLastTimePlayedToDatime1716776027208, +]; diff --git a/src/main/services/aria2c.ts b/src/main/services/aria2c.ts new file mode 100644 index 00000000..b1b1da76 --- /dev/null +++ b/src/main/services/aria2c.ts @@ -0,0 +1,20 @@ +import path from "node:path"; +import { spawn } from "node:child_process"; +import { app } from "electron"; + +export const startAria2 = () => { + const binaryPath = app.isPackaged + ? path.join(process.resourcesPath, "aria2", "aria2c") + : path.join(__dirname, "..", "..", "aria2", "aria2c"); + + return spawn( + binaryPath, + [ + "--enable-rpc", + "--rpc-listen-all", + "--file-allocation=none", + "--allow-overwrite=true", + ], + { stdio: "inherit", windowsHide: true } + ); +}; diff --git a/src/main/services/download-manager.ts b/src/main/services/download-manager.ts index e345835a..60a796a6 100644 --- a/src/main/services/download-manager.ts +++ b/src/main/services/download-manager.ts @@ -1,76 +1,303 @@ -import { gameRepository } from "@main/repository"; +import Aria2, { StatusResponse } from "aria2"; -import type { Game } from "@main/entity"; +import { gameRepository, userPreferencesRepository } from "@main/repository"; + +import { WindowManager } from "./window-manager"; +import { RealDebridClient } from "./real-debrid"; +import { Notification } from "electron"; +import { t } from "i18next"; import { Downloader } from "@shared"; - -import { writePipe } from "./fifo"; -import { RealDebridDownloader } from "./downloaders"; +import { DownloadProgress } from "@types"; +import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; +import { Game } from "@main/entity"; +import { startAria2 } from "./aria2c"; +import { sleep } from "@main/helpers"; +import { logger } from "./logger"; +import type { ChildProcess } from "node:child_process"; export class DownloadManager { - private static gameDownloading: Game; + private static downloads = new Map(); - static async getGame(gameId: number) { - return gameRepository.findOne({ - where: { id: gameId, isDeleted: false }, - relations: { - repack: true, - }, - }); + private static connected = false; + private static gid: string | null = null; + private static game: Game | null = null; + private static realDebridTorrentId: string | null = null; + private static aria2c: ChildProcess | null = null; + + private static aria2 = new Aria2({}); + + private static async connect() { + this.aria2c = startAria2(); + + let retries = 0; + + while (retries < 4 && !this.connected) { + try { + await this.aria2.open(); + logger.log("Connected to aria2"); + + this.connected = true; + } catch (err) { + await sleep(100); + logger.log("Failed to connect to aria2, retrying..."); + retries++; + } + } } - static async cancelDownload() { - if ( - this.gameDownloading && - this.gameDownloading.downloader === Downloader.Torrent - ) { - writePipe.write({ action: "cancel" }); - } else { - RealDebridDownloader.destroy(); + public static disconnect() { + if (this.aria2c) { + this.aria2c.kill(); + } + } + + private static getETA( + totalLength: number, + completedLength: number, + speed: number + ) { + const remainingBytes = totalLength - completedLength; + + if (remainingBytes >= 0 && speed > 0) { + return (remainingBytes / speed) * 1000; + } + + return -1; + } + + static async publishNotification() { + const userPreferences = await userPreferencesRepository.findOne({ + where: { id: 1 }, + }); + + if (userPreferences?.downloadNotificationsEnabled && this.game) { + new Notification({ + title: t("download_complete", { + ns: "notifications", + lng: userPreferences.language, + }), + body: t("game_ready_to_install", { + ns: "notifications", + lng: userPreferences.language, + title: this.game.title, + }), + }).show(); + } + } + + private static getFolderName(status: StatusResponse) { + if (status.bittorrent?.info) return status.bittorrent.info.name; + 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.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); + + const isDownloadingMetadata = status.bittorrent && !status.bittorrent?.info; + + if (status.followedBy?.length) { + this.gid = status.followedBy[0]; + this.downloads.set(this.game.id, this.gid); + return; + } + + const progress = + Number(status.completedLength) / Number(status.totalLength); + + if (!isDownloadingMetadata) { + const update: QueryDeepPartialEntity = { + bytesDownloaded: Number(status.completedLength), + fileSize: Number(status.totalLength), + status: status.status, + }; + + if (!isNaN(progress)) update.progress = progress; + + await gameRepository.update( + { id: this.game.id }, + { + ...update, + status: status.status, + folderName: this.getFolderName(status), + } + ); + } + + const game = await gameRepository.findOne({ + where: { id: this.game.id, isDeleted: false }, + relations: { repack: true }, + }); + + if (progress === 1 && this.game && !isDownloadingMetadata) { + await this.publishNotification(); + + /* + Only cancel bittorrent downloads to stop seeding + */ + if (status.bittorrent) { + await this.cancelDownload(this.game.id); + } else { + this.clearCurrentDownload(); + } + } + + if (WindowManager.mainWindow && game) { + if (!isNaN(progress)) + WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); + + const payload = { + numPeers: Number(status.connections), + numSeeds: Number(status.numSeeders ?? 0), + downloadSpeed: Number(status.downloadSpeed), + timeRemaining: this.getETA( + Number(status.totalLength), + Number(status.completedLength), + Number(status.downloadSpeed) + ), + isDownloadingMetadata: !!isDownloadingMetadata, + game, + } as DownloadProgress; + + WindowManager.mainWindow.webContents.send( + "on-download-progress", + JSON.parse(JSON.stringify(payload)) + ); + } + } + + private static clearCurrentDownload() { + if (this.game) { + this.downloads.delete(this.game.id); + this.gid = null; + this.game = null; + this.realDebridTorrentId = null; + } + } + + static async cancelDownload(gameId: number) { + const gid = this.downloads.get(gameId); + + if (gid) { + await this.aria2.call("remove", gid); + + if (this.gid === gid) { + this.clearCurrentDownload(); + + WindowManager.mainWindow?.setProgressBar(-1); + } else { + this.downloads.delete(gameId); + } } } static async pauseDownload() { - if ( - this.gameDownloading && - this.gameDownloading.downloader === Downloader.Torrent - ) { - writePipe.write({ action: "pause" }); + if (this.gid) { + await this.aria2.call("forcePause", this.gid); + this.gid = null; + } + + this.game = null; + this.realDebridTorrentId = null; + + WindowManager.mainWindow?.setProgressBar(-1); + } + + 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.game = game; + this.realDebridTorrentId = null; } else { - RealDebridDownloader.destroy(); + return this.startDownload(game); } } - static async resumeDownload(gameId: number) { - const game = await this.getGame(gameId); + static async startDownload(game: Game) { + if (!this.connected) await this.connect(); - if (game!.downloader === Downloader.Torrent) { - writePipe.write({ - action: "start", - game_id: game!.id, - magnet: game!.repack.magnet, - save_path: game!.downloadPath, - }); + const options = { + dir: game.downloadPath!, + }; + + if (game.downloader === Downloader.RealDebrid) { + this.realDebridTorrentId = await RealDebridClient.getTorrentId( + game!.repack.magnet + ); } else { - RealDebridDownloader.startDownload(game!); + this.gid = await this.aria2.call("addUri", [game.repack.magnet], options); + this.downloads.set(game.id, this.gid); } - this.gameDownloading = game!; - } - - static async downloadGame(gameId: number) { - const game = await this.getGame(gameId); - - if (game!.downloader === Downloader.Torrent) { - writePipe.write({ - action: "start", - game_id: game!.id, - magnet: game!.repack.magnet, - save_path: game!.downloadPath, - }); - } else { - RealDebridDownloader.startDownload(game!); - } - - this.gameDownloading = game!; + this.game = game; } } diff --git a/src/main/services/downloaders/downloader.ts b/src/main/services/downloaders/downloader.ts deleted file mode 100644 index 14440676..00000000 --- a/src/main/services/downloaders/downloader.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { t } from "i18next"; -import { Notification } from "electron"; - -import { Game } from "@main/entity"; - -import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; - -import { WindowManager } from "../window-manager"; -import type { TorrentUpdate } from "./torrent.downloader"; - -import { GameStatus } from "@shared"; -import { gameRepository, userPreferencesRepository } from "@main/repository"; - -interface DownloadStatus { - numPeers?: number; - numSeeds?: number; - downloadSpeed?: number; - timeRemaining?: number; -} - -export class Downloader { - static getGameProgress(game: Game) { - if (game.status === GameStatus.CheckingFiles) - return game.fileVerificationProgress; - - return game.progress; - } - - static async updateGameProgress( - gameId: number, - gameUpdate: QueryDeepPartialEntity, - downloadStatus: DownloadStatus - ) { - await gameRepository.update({ id: gameId }, gameUpdate); - - const game = await gameRepository.findOne({ - where: { id: gameId, isDeleted: false }, - relations: { repack: true }, - }); - - if (game?.progress === 1) { - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); - - if (userPreferences?.downloadNotificationsEnabled) { - new Notification({ - title: t("download_complete", { - ns: "notifications", - lng: userPreferences.language, - }), - body: t("game_ready_to_install", { - ns: "notifications", - lng: userPreferences.language, - title: game?.title, - }), - }).show(); - } - } - - if (WindowManager.mainWindow && game) { - const progress = this.getGameProgress(game); - WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); - - WindowManager.mainWindow.webContents.send( - "on-download-progress", - JSON.parse( - JSON.stringify({ - ...({ - progress: gameUpdate.progress, - bytesDownloaded: gameUpdate.bytesDownloaded, - fileSize: gameUpdate.fileSize, - gameId, - numPeers: downloadStatus.numPeers, - numSeeds: downloadStatus.numSeeds, - downloadSpeed: downloadStatus.downloadSpeed, - timeRemaining: downloadStatus.timeRemaining, - } as TorrentUpdate), - game, - }) - ) - ); - } - } -} diff --git a/src/main/services/downloaders/index.ts b/src/main/services/downloaders/index.ts deleted file mode 100644 index cd742107..00000000 --- a/src/main/services/downloaders/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./real-debrid.downloader"; -export * from "./torrent.downloader"; diff --git a/src/main/services/downloaders/real-debrid.downloader.ts b/src/main/services/downloaders/real-debrid.downloader.ts deleted file mode 100644 index 8a44f934..00000000 --- a/src/main/services/downloaders/real-debrid.downloader.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Game } from "@main/entity"; -import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; -import path from "node:path"; -import fs from "node:fs"; -import EasyDL from "easydl"; -import { GameStatus } from "@shared"; -// import { fullArchive } from "node-7z-archive"; - -import { Downloader } from "./downloader"; -import { RealDebridClient } from "../real-debrid"; - -export class RealDebridDownloader extends Downloader { - private static download: EasyDL; - private static downloadSize = 0; - - private static getEta(bytesDownloaded: number, speed: number) { - const remainingBytes = this.downloadSize - bytesDownloaded; - - if (remainingBytes >= 0 && speed > 0) { - return (remainingBytes / speed) * 1000; - } - - return 1; - } - - private static createFolderIfNotExists(path: string) { - if (!fs.existsSync(path)) { - fs.mkdirSync(path); - } - } - - // private static async startDecompression( - // rarFile: string, - // dest: string, - // game: Game - // ) { - // await fullArchive(rarFile, dest); - - // const updatePayload: QueryDeepPartialEntity = { - // status: GameStatus.Finished, - // }; - - // await this.updateGameProgress(game.id, updatePayload, {}); - // } - - static destroy() { - if (this.download) { - this.download.destroy(); - } - } - - static async startDownload(game: Game) { - if (this.download) this.download.destroy(); - const downloadUrl = decodeURIComponent( - await RealDebridClient.getDownloadUrl(game) - ); - - const filename = path.basename(downloadUrl); - const folderName = path.basename(filename, path.extname(filename)); - - const downloadPath = path.join(game.downloadPath!, folderName); - this.createFolderIfNotExists(downloadPath); - - this.download = new EasyDL(downloadUrl, path.join(downloadPath, filename)); - - const metadata = await this.download.metadata(); - - this.downloadSize = metadata.size; - - const updatePayload: QueryDeepPartialEntity = { - status: GameStatus.Downloading, - fileSize: metadata.size, - folderName, - }; - - const downloadStatus = { - timeRemaining: Number.POSITIVE_INFINITY, - }; - - await this.updateGameProgress(game.id, updatePayload, downloadStatus); - - this.download.on("progress", async ({ total }) => { - const updatePayload: QueryDeepPartialEntity = { - status: GameStatus.Downloading, - progress: Math.min(0.99, total.percentage / 100), - bytesDownloaded: total.bytes, - }; - - const downloadStatus = { - downloadSpeed: total.speed, - timeRemaining: this.getEta(total.bytes ?? 0, total.speed ?? 0), - }; - - await this.updateGameProgress(game.id, updatePayload, downloadStatus); - }); - - this.download.on("end", async () => { - const updatePayload: QueryDeepPartialEntity = { - status: GameStatus.Finished, - progress: 1, - }; - - await this.updateGameProgress(game.id, updatePayload, { - timeRemaining: 0, - }); - - /* This has to be improved */ - // this.startDecompression( - // path.join(downloadPath, filename), - // downloadPath, - // game - // ); - }); - } -} diff --git a/src/main/services/downloaders/torrent.downloader.ts b/src/main/services/downloaders/torrent.downloader.ts deleted file mode 100644 index d5e039a8..00000000 --- a/src/main/services/downloaders/torrent.downloader.ts +++ /dev/null @@ -1,156 +0,0 @@ -import path from "node:path"; -import cp from "node:child_process"; -import fs from "node:fs"; -import { app, dialog } from "electron"; -import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; - -import { Game } from "@main/entity"; -import { GameStatus } from "@shared"; -import { Downloader } from "./downloader"; -import { readPipe, writePipe } from "../fifo"; - -const binaryNameByPlatform: Partial> = { - darwin: "hydra-download-manager", - linux: "hydra-download-manager", - win32: "hydra-download-manager.exe", -}; - -enum TorrentState { - CheckingFiles = 1, - DownloadingMetadata = 2, - Downloading = 3, - Finished = 4, - Seeding = 5, -} - -export interface TorrentUpdate { - gameId: number; - progress: number; - downloadSpeed: number; - timeRemaining: number; - numPeers: number; - numSeeds: number; - status: TorrentState; - folderName: string; - fileSize: number; - bytesDownloaded: number; -} - -export const BITTORRENT_PORT = "5881"; - -export class TorrentDownloader extends Downloader { - private static messageLength = 1024 * 2; - - public static async attachListener() { - // eslint-disable-next-line no-constant-condition - while (true) { - const buffer = readPipe.socket?.read(this.messageLength); - - if (buffer === null) { - await new Promise((resolve) => setTimeout(resolve, 100)); - continue; - } - - const message = Buffer.from( - buffer.slice(0, buffer.indexOf(0x00)) - ).toString("utf-8"); - - try { - const payload = JSON.parse(message) as TorrentUpdate; - - const updatePayload: QueryDeepPartialEntity = { - bytesDownloaded: payload.bytesDownloaded, - status: this.getTorrentStateName(payload.status), - }; - - if (payload.status === TorrentState.CheckingFiles) { - updatePayload.fileVerificationProgress = payload.progress; - } else { - if (payload.folderName) { - updatePayload.folderName = payload.folderName; - updatePayload.fileSize = payload.fileSize; - } - } - - if ( - [TorrentState.Downloading, TorrentState.Seeding].includes( - payload.status - ) - ) { - updatePayload.progress = payload.progress; - } - - this.updateGameProgress(payload.gameId, updatePayload, { - numPeers: payload.numPeers, - numSeeds: payload.numSeeds, - downloadSpeed: payload.downloadSpeed, - timeRemaining: payload.timeRemaining, - }); - } finally { - await new Promise((resolve) => setTimeout(resolve, 100)); - } - } - } - - public static startClient() { - return new Promise((resolve) => { - const commonArgs = [ - BITTORRENT_PORT, - writePipe.socketPath, - readPipe.socketPath, - ]; - - if (app.isPackaged) { - const binaryName = binaryNameByPlatform[process.platform]!; - const binaryPath = path.join( - process.resourcesPath, - "hydra-download-manager", - binaryName - ); - - if (!fs.existsSync(binaryPath)) { - dialog.showErrorBox( - "Fatal", - "Hydra download manager binary not found. Please check if it has been removed by Windows Defender." - ); - - app.quit(); - } - - cp.spawn(binaryPath, commonArgs, { - stdio: "inherit", - windowsHide: true, - }); - } else { - const scriptPath = path.join( - __dirname, - "..", - "..", - "torrent-client", - "main.py" - ); - - cp.spawn("python3", [scriptPath, ...commonArgs], { - stdio: "inherit", - }); - } - - Promise.all([writePipe.createPipe(), readPipe.createPipe()]).then( - async () => { - this.attachListener(); - resolve(null); - } - ); - }); - } - - private static getTorrentStateName(state: TorrentState) { - if (state === TorrentState.CheckingFiles) return GameStatus.CheckingFiles; - if (state === TorrentState.Downloading) return GameStatus.Downloading; - if (state === TorrentState.DownloadingMetadata) - return GameStatus.DownloadingMetadata; - if (state === TorrentState.Finished) return GameStatus.Finished; - if (state === TorrentState.Seeding) return GameStatus.Seeding; - return null; - } -} diff --git a/src/main/services/fifo.ts b/src/main/services/fifo.ts deleted file mode 100644 index 866232cc..00000000 --- a/src/main/services/fifo.ts +++ /dev/null @@ -1,38 +0,0 @@ -import path from "node:path"; -import net from "node:net"; -import crypto from "node:crypto"; -import os from "node:os"; - -export class FIFO { - public socket: null | net.Socket = null; - public socketPath = this.generateSocketFilename(); - - private generateSocketFilename() { - const hash = crypto.randomBytes(16).toString("hex"); - - if (process.platform === "win32") { - return "\\\\.\\pipe\\" + hash; - } - - return path.join(os.tmpdir(), hash); - } - - public write(data: any) { - if (!this.socket) return; - this.socket.write(Buffer.from(JSON.stringify(data))); - } - - public createPipe() { - return new Promise((resolve) => { - const server = net.createServer((socket) => { - this.socket = socket; - resolve(null); - }); - - server.listen(this.socketPath); - }); - } -} - -export const writePipe = new FIFO(); -export const readPipe = new FIFO(); diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 4b13d38d..776fd7f6 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -5,8 +5,7 @@ export * from "./steam-250"; export * from "./steam-grid"; export * from "./update-resolver"; export * from "./window-manager"; -export * from "./fifo"; -export * from "./downloaders"; export * from "./download-manager"; export * from "./how-long-to-beat"; export * from "./process-watcher"; +export * from "./main-loop"; diff --git a/src/main/services/main-loop.ts b/src/main/services/main-loop.ts new file mode 100644 index 00000000..dfae5eec --- /dev/null +++ b/src/main/services/main-loop.ts @@ -0,0 +1,15 @@ +import { sleep } from "@main/helpers"; +import { DownloadManager } from "./download-manager"; +import { watchProcesses } from "./process-watcher"; + +export const startMainLoop = async () => { + // eslint-disable-next-line no-constant-condition + while (true) { + await Promise.allSettled([ + watchProcesses(), + DownloadManager.watchDownloads(), + ]); + + await sleep(500); + } +}; diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 16646934..ea1b6355 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -5,73 +5,58 @@ import { gameRepository } from "@main/repository"; import { getProcesses } from "@main/helpers"; import { WindowManager } from "./window-manager"; -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +const gamesPlaytime = new Map(); -export const startProcessWatcher = async () => { - const sleepTime = 500; - const gamesPlaytime = new Map(); +export const watchProcesses = async () => { + const games = await gameRepository.find({ + where: { + executablePath: Not(IsNull()), + isDeleted: false, + }, + }); - // eslint-disable-next-line no-constant-condition - while (true) { - const games = await gameRepository.find({ - where: { - executablePath: Not(IsNull()), - isDeleted: false, - }, + if (games.length === 0) return; + + const processes = await getProcesses(); + + for (const game of games) { + const executablePath = game.executablePath!; + const basename = path.win32.basename(executablePath); + const basenameWithoutExtension = path.win32.basename( + executablePath, + path.extname(executablePath) + ); + + const gameProcess = processes.find((runningProcess) => { + if (process.platform === "win32") { + return runningProcess.name === basename; + } + + return [basename, basenameWithoutExtension].includes(runningProcess.name); }); - if (games.length === 0) { - await sleep(sleepTime); - continue; - } + if (gameProcess) { + if (gamesPlaytime.has(game.id)) { + const zero = gamesPlaytime.get(game.id) ?? 0; + const delta = performance.now() - zero; - const processes = await getProcesses(); - - for (const game of games) { - const executablePath = game.executablePath!; - const basename = path.win32.basename(executablePath); - const basenameWithoutExtension = path.win32.basename( - executablePath, - path.extname(executablePath) - ); - - const gameProcess = processes.find((runningProcess) => { - if (process.platform === "win32") { - return runningProcess.name === basename; - } - - return [basename, basenameWithoutExtension].includes( - runningProcess.name - ); - }); - - if (gameProcess) { - if (gamesPlaytime.has(game.id)) { - const zero = gamesPlaytime.get(game.id) ?? 0; - const delta = performance.now() - zero; - - if (WindowManager.mainWindow) { - WindowManager.mainWindow.webContents.send("on-playtime", game.id); - } - - await gameRepository.update(game.id, { - playTimeInMilliseconds: game.playTimeInMilliseconds + delta, - }); - - gameRepository.update(game.id, { - lastTimePlayed: new Date().toUTCString(), - }); - } - - gamesPlaytime.set(game.id, performance.now()); - } else if (gamesPlaytime.has(game.id)) { - gamesPlaytime.delete(game.id); if (WindowManager.mainWindow) { - WindowManager.mainWindow.webContents.send("on-game-close", game.id); + WindowManager.mainWindow.webContents.send("on-playtime", game.id); } + + await gameRepository.update(game.id, { + playTimeInMilliseconds: game.playTimeInMilliseconds + delta, + lastTimePlayed: new Date(), + }); + } + + gamesPlaytime.set(game.id, performance.now()); + } else if (gamesPlaytime.has(game.id)) { + gamesPlaytime.delete(game.id); + + if (WindowManager.mainWindow) { + WindowManager.mainWindow.webContents.send("on-game-close", game.id); } } - - await sleep(sleepTime); } }; diff --git a/src/main/services/real-debrid.ts b/src/main/services/real-debrid.ts index 355a59b3..2e0debe6 100644 --- a/src/main/services/real-debrid.ts +++ b/src/main/services/real-debrid.ts @@ -1,15 +1,24 @@ -import { Game } from "@main/entity"; +import axios, { AxiosInstance } from "axios"; +import parseTorrent from "parse-torrent"; import type { RealDebridAddMagnet, RealDebridTorrentInfo, RealDebridUnrestrictLink, -} from "./real-debrid.types"; -import axios, { AxiosInstance } from "axios"; - -const base = "https://api.real-debrid.com/rest/1.0"; + RealDebridUser, +} from "@types"; 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 }); @@ -22,13 +31,18 @@ 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}` ); return response.data; } + static async getUser() { + const response = await this.instance.get(`/user`); + return response.data; + } + static async selectAllFiles(id: string) { const searchParams = new URLSearchParams({ files: "all" }); @@ -49,51 +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 + ); - if (!torrent) { - const magnet = await RealDebridClient.addMagnet(game!.repack.magnet); + if (userTorrent) return userTorrent.id; - if (magnet && magnet.id) { - await RealDebridClient.selectAllFiles(magnet.id); - torrent = await RealDebridClient.getInfo(magnet.id); - } - } - - if (torrent) { - 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 async 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/main/services/real-debrid.types.ts b/src/main/services/real-debrid.types.ts deleted file mode 100644 index 6707641f..00000000 --- a/src/main/services/real-debrid.types.ts +++ /dev/null @@ -1,51 +0,0 @@ -export interface RealDebridUnrestrictLink { - id: string; - filename: string; - mimeType: string; - filesize: number; - link: string; - host: string; - host_icon: string; - chunks: number; - crc: number; - download: string; - streamable: number; -} - -export interface RealDebridAddMagnet { - id: string; - // URL of the created ressource - uri: string; -} - -export interface RealDebridTorrentInfo { - id: string; - filename: string; - original_filename: string; // Original name of the torrent - hash: string; // SHA1 Hash of the torrent - bytes: number; // Size of selected files only - original_bytes: number; // Total size of the torrent - host: string; // Host main domain - split: number; // Split size of links - progress: number; // Possible values: 0 to 100 - status: string; // Current status of the torrent: magnet_error, magnet_conversion, waiting_files_selection, queued, downloading, downloaded, error, virus, compressing, uploading, dead - added: string; // jsonDate - files: [ - { - id: number; - path: string; // Path to the file inside the torrent, starting with "/" - bytes: number; - selected: number; // 0 or 1 - }, - { - id: number; - path: string; // Path to the file inside the torrent, starting with "/" - bytes: number; - selected: number; // 0 or 1 - }, - ]; - links: string[]; - ended: string; // !! Only present when finished, jsonDate - speed: number; // !! Only present in "downloading", "compressing", "uploading" status - seeders: number; // !! Only present in "downloading", "magnet_conversion" status -} diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 7b2c87b7..c380821e 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -17,9 +17,6 @@ import { IsNull, Not } from "typeorm"; export class WindowManager { public static mainWindow: Electron.BrowserWindow | null = null; - public static splashWindow: Electron.BrowserWindow | null = null; - public static isReadyToShowMainWindow = false; - private static isMainMaximized = false; private static loadURL(hash = "") { // HMR for renderer base on electron-vite cli. @@ -38,48 +35,8 @@ export class WindowManager { } } - private static loadSplashURL() { - // HMR for renderer base on electron-vite cli. - // Load the remote URL for development or the local html file for production. - if (is.dev && process.env["ELECTRON_RENDERER_URL"]) { - this.splashWindow?.loadURL( - `${process.env["ELECTRON_RENDERER_URL"]}#/splash` - ); - } else { - this.splashWindow?.loadFile( - path.join(__dirname, "../renderer/index.html"), - { - hash: "splash", - } - ); - } - } - - public static createSplashScreen() { - if (this.splashWindow) return; - - this.splashWindow = new BrowserWindow({ - width: 380, - height: 380, - frame: false, - resizable: false, - backgroundColor: "#1c1c1c", - webPreferences: { - preload: path.join(__dirname, "../preload/index.mjs"), - sandbox: false, - }, - }); - - this.loadSplashURL(); - this.splashWindow.removeMenu(); - if (this.splashWindow?.isMaximized()) { - this.splashWindow?.unmaximize(); - this.isMainMaximized = true; - } - } - public static createMainWindow() { - if (this.mainWindow || !this.isReadyToShowMainWindow) return; + if (this.mainWindow) return; this.mainWindow = new BrowserWindow({ width: 1200, @@ -104,7 +61,6 @@ export class WindowManager { this.loadURL(); this.mainWindow.removeMenu(); - if (this.isMainMaximized) this.mainWindow?.maximize(); this.mainWindow.on("ready-to-show", () => { if (!app.isPackaged) WindowManager.mainWindow?.webContents.openDevTools(); @@ -123,12 +79,6 @@ export class WindowManager { }); } - public static prepareMainWindowAndCloseSplash() { - this.isReadyToShowMainWindow = true; - this.splashWindow?.close(); - this.createMainWindow(); - } - public static redirect(hash: string) { if (!this.mainWindow) this.createMainWindow(); this.loadURL(hash); @@ -149,7 +99,7 @@ export class WindowManager { }, take: 5, order: { - updatedAt: "DESC", + lastTimePlayed: "DESC", }, }); diff --git a/src/preload/index.ts b/src/preload/index.ts index 4ddf5009..74f2e767 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -5,38 +5,26 @@ import { contextBridge, ipcRenderer } from "electron"; import type { CatalogueCategory, GameShop, - TorrentProgress, + DownloadProgress, UserPreferences, - AppUpdaterEvents, + AppUpdaterEvent, + StartGameDownloadPayload, } from "@types"; contextBridge.exposeInMainWorld("electron", { /* Torrenting */ - startGameDownload: ( - repackId: number, - objectID: string, - title: string, - shop: GameShop, - downloadPath: string - ) => - ipcRenderer.invoke( - "startGameDownload", - repackId, - objectID, - title, - shop, - downloadPath - ), + startGameDownload: (payload: StartGameDownloadPayload) => + ipcRenderer.invoke("startGameDownload", payload), cancelGameDownload: (gameId: number) => ipcRenderer.invoke("cancelGameDownload", gameId), pauseGameDownload: (gameId: number) => ipcRenderer.invoke("pauseGameDownload", gameId), resumeGameDownload: (gameId: number) => ipcRenderer.invoke("resumeGameDownload", gameId), - onDownloadProgress: (cb: (value: TorrentProgress) => void) => { + onDownloadProgress: (cb: (value: DownloadProgress) => void) => { const listener = ( _event: Electron.IpcRendererEvent, - value: TorrentProgress + value: DownloadProgress ) => cb(value); ipcRenderer.on("on-download-progress", listener); return () => ipcRenderer.removeListener("on-download-progress", listener); @@ -61,6 +49,8 @@ contextBridge.exposeInMainWorld("electron", { updateUserPreferences: (preferences: UserPreferences) => ipcRenderer.invoke("updateUserPreferences", preferences), autoLaunch: (enabled: boolean) => ipcRenderer.invoke("autoLaunch", enabled), + authenticateRealDebrid: (apiToken: string) => + ipcRenderer.invoke("authenticateRealDebrid", apiToken), /* Library */ addGameToLibrary: ( @@ -84,6 +74,7 @@ contextBridge.exposeInMainWorld("electron", { closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId), removeGameFromLibrary: (gameId: number) => ipcRenderer.invoke("removeGameFromLibrary", gameId), + removeGame: (gameId: number) => ipcRenderer.invoke("removeGame", gameId), deleteGameFolder: (gameId: number) => ipcRenderer.invoke("deleteGameFolder", gameId), getGameByObjectID: (objectID: string) => @@ -114,11 +105,11 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("showOpenDialog", options), platform: process.platform, - /* Splash */ - onAutoUpdaterEvent: (cb: (value: AppUpdaterEvents) => void) => { + /* Auto update */ + onAutoUpdaterEvent: (cb: (value: AppUpdaterEvent) => void) => { const listener = ( _event: Electron.IpcRendererEvent, - value: AppUpdaterEvents + value: AppUpdaterEvent ) => cb(value); ipcRenderer.on("autoUpdaterEvent", listener); @@ -129,5 +120,4 @@ contextBridge.exposeInMainWorld("electron", { }, checkForUpdates: () => ipcRenderer.invoke("checkForUpdates"), restartAndInstallUpdate: () => ipcRenderer.invoke("restartAndInstallUpdate"), - continueToMainWindow: () => ipcRenderer.invoke("continueToMainWindow"), }); diff --git a/src/renderer/src/app.css.ts b/src/renderer/src/app.css.ts index 2a55f033..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]", { @@ -79,6 +79,10 @@ globalStyle("img", { WebkitUserDrag: "none", } as Record); +globalStyle("progress[value]", { + WebkitAppearance: "none", +}); + export const container = style({ width: "100%", height: "100%", diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index a1dd3d9a..325e1506 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef } from "react"; -import { Sidebar, BottomPanel, Header } from "@renderer/components"; +import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components"; import { useAppDispatch, @@ -18,8 +18,8 @@ import { clearSearch, setUserPreferences, toggleDraggingDisabled, + closeToast, } from "@renderer/features"; -import { GameStatusHelper } from "@shared"; document.body.classList.add(themeClass); @@ -42,6 +42,7 @@ export function App() { const draggingDisabled = useAppSelector( (state) => state.window.draggingDisabled ); + const toast = useAppSelector((state) => state.toast); useEffect(() => { Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then( @@ -54,7 +55,7 @@ export function App() { useEffect(() => { const unsubscribe = window.electron.onDownloadProgress( (downloadProgress) => { - if (GameStatusHelper.isReady(downloadProgress.game.status)) { + if (downloadProgress.game.progress === 1) { clearDownload(); updateLibrary(); return; @@ -109,6 +110,10 @@ export function App() { }); }, [dispatch, draggingDisabled]); + const handleToastClose = useCallback(() => { + dispatch(closeToast()); + }, [dispatch]); + return ( <> {window.electron.platform === "win32" && ( @@ -132,7 +137,15 @@ export function App() { + + + ); } diff --git a/src/renderer/src/assets/epic-games-logo.svg b/src/renderer/src/assets/epic-games-logo.svg deleted file mode 100644 index a6c53dbc..00000000 --- a/src/renderer/src/assets/epic-games-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/renderer/src/assets/icon.png b/src/renderer/src/assets/icon.png deleted file mode 100644 index 9254a8fb..00000000 Binary files a/src/renderer/src/assets/icon.png and /dev/null differ diff --git a/src/renderer/src/assets/telegram-icon.svg b/src/renderer/src/assets/telegram-icon.svg deleted file mode 100644 index 962ab45f..00000000 --- a/src/renderer/src/assets/telegram-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/renderer/src/assets/x-icon.svg b/src/renderer/src/assets/x-icon.svg deleted file mode 100644 index c394d154..00000000 --- a/src/renderer/src/assets/x-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/renderer/src/components/backdrop/backdrop.css.ts b/src/renderer/src/components/backdrop/backdrop.css.ts index 0a7b61bb..3b8cc4e2 100644 --- a/src/renderer/src/components/backdrop/backdrop.css.ts +++ b/src/renderer/src/components/backdrop/backdrop.css.ts @@ -43,5 +43,11 @@ export const backdrop = recipe({ backgroundColor: "rgba(0, 0, 0, 0)", }, }, + windows: { + true: { + // SPACING_UNIT * 3 + title bar spacing + paddingTop: `${SPACING_UNIT * 3 + 35}px`, + }, + }, }, }); diff --git a/src/renderer/src/components/backdrop/backdrop.tsx b/src/renderer/src/components/backdrop/backdrop.tsx index 5852d59d..f498e664 100644 --- a/src/renderer/src/components/backdrop/backdrop.tsx +++ b/src/renderer/src/components/backdrop/backdrop.tsx @@ -7,6 +7,13 @@ export interface BackdropProps { export function Backdrop({ isClosing = false, children }: BackdropProps) { return ( -
{children}
+
+ {children} +
); } 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 f339e0d5..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,19 +4,21 @@ 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", transition: "all ease 0.2s", justifyContent: "space-between", + position: "relative", zIndex: "1", }); 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/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx index 44d125cd..db654c8a 100644 --- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx +++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx @@ -1,23 +1,21 @@ +import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDownload } from "@renderer/hooks"; import * as styles from "./bottom-panel.css"; -import { vars } from "../../theme.css"; -import { useEffect, useMemo, useState } from "react"; + import { useNavigate } from "react-router-dom"; import { VERSION_CODENAME } from "@renderer/constants"; -import { GameStatus, GameStatusHelper } from "@shared"; export function BottomPanel() { const { t } = useTranslation("bottom_panel"); const navigate = useNavigate(); - const { game, progress, downloadSpeed, eta } = useDownload(); + const { lastPacket, progress, downloadSpeed, eta } = useDownload(); - const isGameDownloading = - game && GameStatusHelper.isDownloading(game.status ?? null); + const isGameDownloading = !!lastPacket?.game; const [version, setVersion] = useState(""); @@ -27,17 +25,18 @@ export function BottomPanel() { const status = useMemo(() => { if (isGameDownloading) { - if (game.status === GameStatus.DownloadingMetadata) - return t("downloading_metadata", { title: game.title }); + if (lastPacket?.isDownloadingMetadata) + return t("downloading_metadata", { title: lastPacket?.game.title }); - if (game.status === GameStatus.CheckingFiles) - return t("checking_files", { - title: game.title, + if (!eta) { + return t("calculating_eta", { + title: lastPacket?.game.title, percentage: progress, }); + } return t("downloading", { - title: game?.title, + title: lastPacket?.game.title, percentage: progress, eta, speed: downloadSpeed, @@ -45,17 +44,18 @@ export function BottomPanel() { } return t("no_downloads_in_progress"); - }, [t, isGameDownloading, game, progress, eta, downloadSpeed]); + }, [ + t, + isGameDownloading, + lastPacket?.game, + lastPacket?.isDownloadingMetadata, + progress, + eta, + downloadSpeed, + ]); return ( -