feat: updating real-debrid translations

This commit is contained in:
Chubby Granny Chaser 2024-05-29 16:22:30 +01:00
parent 183b85d66a
commit 08a336b392
No known key found for this signature in database
38 changed files with 284 additions and 228 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
.vscode .vscode
node_modules node_modules
aria2* aria2/
fastlist.exe fastlist.exe
__pycache__ __pycache__
dist dist

View File

@ -134,7 +134,7 @@
"delete_modal_title": "هل أنت متأكد؟", "delete_modal_title": "هل أنت متأكد؟",
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهاز الكمبيوتر الخاص بك", "delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهاز الكمبيوتر الخاص بك",
"install": "تثبيت", "install": "تثبيت",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"torrent": "تورنت" "torrent": "تورنت"
}, },
"settings": { "settings": {
@ -145,13 +145,13 @@
"enable_repack_list_notifications": "عند إضافة حزمة جديدة", "enable_repack_list_notifications": "عند إضافة حزمة جديدة",
"telemetry": "القياس عن بعد", "telemetry": "القياس عن بعد",
"telemetry_description": "تفعيل إحصائيات الاستخدام مجهولة المصدر", "telemetry_description": "تفعيل إحصائيات الاستخدام مجهولة المصدر",
"real_debrid_api_token_label": "رمز واجهة برمجة التطبيقات (API) لـReal Debrid ", "real_debrid_api_token_label": "رمز واجهة برمجة التطبيقات (API) لـReal-Debrid ",
"quit_app_instead_hiding": "إنهاء هايدرا بدلاً من التصغير الى شريط الحالة", "quit_app_instead_hiding": "إنهاء هايدرا بدلاً من التصغير الى شريط الحالة",
"launch_with_system": "تشغيل هايدرا عند بدء تشغيل النظام", "launch_with_system": "تشغيل هايدرا عند بدء تشغيل النظام",
"general": "عام", "general": "عام",
"behavior": "السلوك", "behavior": "السلوك",
"enable_real_debrid": "تفعيل Real Debrid ", "enable_real_debrid": "تفعيل Real-Debrid ",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"real_debrid_api_token_hint": "يمكنك الحصول على مفتاح API الخاص بك هنا.", "real_debrid_api_token_hint": "يمكنك الحصول على مفتاح API الخاص بك هنا.",
"save_changes": "حفظ التغييرات" "save_changes": "حفظ التغييرات"
}, },

View File

@ -128,7 +128,7 @@
"delete_modal_title": "Er du sikker?", "delete_modal_title": "Er du sikker?",
"delete_modal_description": "Dette vil fjerne alle installations filerne fra din computer", "delete_modal_description": "Dette vil fjerne alle installations filerne fra din computer",
"install": "Installér", "install": "Installér",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"torrent": "Torrent" "torrent": "Torrent"
}, },
"settings": { "settings": {
@ -139,13 +139,13 @@
"enable_repack_list_notifications": "Når en ny repack bliver tilføjet", "enable_repack_list_notifications": "Når en ny repack bliver tilføjet",
"telemetry": "Telemetri", "telemetry": "Telemetri",
"telemetry_description": "Slå anonymt brugs statistik til", "telemetry_description": "Slå anonymt brugs statistik til",
"real_debrid_api_token_description": "Real Debrid API token", "real_debrid_api_token_description": "Real-Debrid API token",
"quit_app_instead_hiding": "Afslut Hydra instedet for at minimere til processlinjen", "quit_app_instead_hiding": "Afslut Hydra instedet for at minimere til processlinjen",
"launch_with_system": "Åben Hydra ved start af systemet", "launch_with_system": "Åben Hydra ved start af systemet",
"general": "Generelt", "general": "Generelt",
"behavior": "Opførsel", "behavior": "Opførsel",
"enable_real_debrid": "Slå Real Debrid til", "enable_real_debrid": "Slå Real-Debrid til",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"real_debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>.", "real_debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>.",
"save_changes": "Gem ændringer" "save_changes": "Gem ændringer"
}, },

View File

@ -133,7 +133,7 @@
"delete_modal_title": "Are you sure?", "delete_modal_title": "Are you sure?",
"delete_modal_description": "This will remove all the installation files from your computer", "delete_modal_description": "This will remove all the installation files from your computer",
"install": "Install", "install": "Install",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"torrent": "Torrent" "torrent": "Torrent"
}, },
"settings": { "settings": {
@ -144,14 +144,15 @@
"enable_repack_list_notifications": "When a new repack is added", "enable_repack_list_notifications": "When a new repack is added",
"telemetry": "Telemetry", "telemetry": "Telemetry",
"telemetry_description": "Enable anonymous usage statistics", "telemetry_description": "Enable anonymous usage statistics",
"real_debrid_api_token_label": "Real Debrid API token", "real_debrid_api_token_label": "Real-Debrid API token",
"quit_app_instead_hiding": "Quit Hydra instead of minimizing to tray", "quit_app_instead_hiding": "Quit Hydra instead of minimizing to tray",
"launch_with_system": "Launch Hydra on system start-up", "launch_with_system": "Launch Hydra on system start-up",
"general": "General", "general": "General",
"behavior": "Behavior", "behavior": "Behavior",
"enable_real_debrid": "Enable Real Debrid", "enable_real_debrid": "Enable Real-Debrid",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"real_debrid_api_token_hint": "You can get your API key <0>here</0>.", "real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to download files instantly and at the best of your Internet speed.",
"real_debrid_api_token_hint": "You can get your API token <0>here</0>.",
"save_changes": "Save changes" "save_changes": "Save changes"
}, },
"notifications": { "notifications": {

View File

@ -134,7 +134,7 @@
"delete_modal_title": "¿Estás seguro?", "delete_modal_title": "¿Estás seguro?",
"delete_modal_description": "Esto eliminará todos los archivos de instalación de tu computadora.", "delete_modal_description": "Esto eliminará todos los archivos de instalación de tu computadora.",
"install": "Instalar", "install": "Instalar",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"torrent": "Torrent" "torrent": "Torrent"
}, },
"settings": { "settings": {
@ -145,13 +145,13 @@
"enable_repack_list_notifications": "Cuando se añade un repack nuevo", "enable_repack_list_notifications": "Cuando se añade un repack nuevo",
"telemetry": "Telemetría", "telemetry": "Telemetría",
"telemetry_description": "Habilitar recopilación de datos de manera anónima", "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", "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", "launch_with_system": "Iniciar Hydra al inicio del sistema",
"general": "General", "general": "General",
"behavior": "Otros", "behavior": "Otros",
"enable_real_debrid": "Activar Real Debrid", "enable_real_debrid": "Activar Real-Debrid",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>.", "real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>.",
"save_changes": "Guardar cambios" "save_changes": "Guardar cambios"
}, },

View File

@ -128,7 +128,7 @@
"delete_modal_title": "مطمئنی؟", "delete_modal_title": "مطمئنی؟",
"delete_modal_description": "این کار تمام فایل‌های اینستالر را از کامپیوتر شما حذف می‌کند", "delete_modal_description": "این کار تمام فایل‌های اینستالر را از کامپیوتر شما حذف می‌کند",
"install": "نصف", "install": "نصف",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"torrent": "تورنت" "torrent": "تورنت"
}, },
"settings": { "settings": {
@ -139,13 +139,13 @@
"enable_repack_list_notifications": "زمانی که یک ریپک جدید اضافه شد", "enable_repack_list_notifications": "زمانی که یک ریپک جدید اضافه شد",
"telemetry": "تلمتری", "telemetry": "تلمتری",
"telemetry_description": "فعال کردن آمارگیری استفاده ناشناس", "telemetry_description": "فعال کردن آمارگیری استفاده ناشناس",
"real_debrid_api_token_description": "توکن Real Debrid", "real_debrid_api_token_description": "توکن Real-Debrid",
"quit_app_instead_hiding": "به جای کوچک کردن، از هایدرا خارج شو", "quit_app_instead_hiding": "به جای کوچک کردن، از هایدرا خارج شو",
"launch_with_system": "زمانی که سیستم روشن می‌شود، هایدرا را باز کن", "launch_with_system": "زمانی که سیستم روشن می‌شود، هایدرا را باز کن",
"general": "کلی", "general": "کلی",
"behavior": "رفتار", "behavior": "رفتار",
"enable_real_debrid": "فعال‌سازی Real Debrid", "enable_real_debrid": "فعال‌سازی Real-Debrid",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"real_debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.", "real_debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.",
"save_changes": "ذخیره تغییرات" "save_changes": "ذخیره تغییرات"
}, },

View File

@ -128,7 +128,7 @@
"delete_modal_title": "정말로 하시겠습니까?", "delete_modal_title": "정말로 하시겠습니까?",
"delete_modal_description": "이 기기의 모든 설치 파일들이 제거될 것입니다", "delete_modal_description": "이 기기의 모든 설치 파일들이 제거될 것입니다",
"install": "설치", "install": "설치",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"torrent": "Torrent" "torrent": "Torrent"
}, },
"settings": { "settings": {
@ -139,13 +139,13 @@
"enable_repack_list_notifications": "새 리팩이 추가되었을 때", "enable_repack_list_notifications": "새 리팩이 추가되었을 때",
"telemetry": "자동 데이터 수집", "telemetry": "자동 데이터 수집",
"telemetry_description": "익명 사용 통계를 활성화", "telemetry_description": "익명 사용 통계를 활성화",
"real_debrid_api_token_description": "Real Debrid API 토큰", "real_debrid_api_token_description": "Real-Debrid API 토큰",
"quit_app_instead_hiding": "작업 표시줄 트레이로 최소화하는 대신 Hydra를 종료", "quit_app_instead_hiding": "작업 표시줄 트레이로 최소화하는 대신 Hydra를 종료",
"launch_with_system": "컴퓨터가 시작되었을 때 Hydra 실행", "launch_with_system": "컴퓨터가 시작되었을 때 Hydra 실행",
"general": "일반", "general": "일반",
"behavior": "행동", "behavior": "행동",
"enable_real_debrid": "Real Debrid 활성화", "enable_real_debrid": "Real-Debrid 활성화",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"real_debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.", "real_debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.",
"save_changes": "변경 사항 저장" "save_changes": "변경 사항 저장"
}, },

View File

@ -128,7 +128,7 @@
"delete_modal_title": "Weet je het zeker?", "delete_modal_title": "Weet je het zeker?",
"delete_modal_description": "Hiermee worden alle installatiebestanden van uw computer verwijderd", "delete_modal_description": "Hiermee worden alle installatiebestanden van uw computer verwijderd",
"install": "Installeren", "install": "Installeren",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"torrent": "Torrent" "torrent": "Torrent"
}, },
"settings": { "settings": {
@ -139,13 +139,13 @@
"enable_repack_list_notifications": "Wanneer een nieuwe herverpakking wordt toegevoegd", "enable_repack_list_notifications": "Wanneer een nieuwe herverpakking wordt toegevoegd",
"telemetry": "Telemetrie", "telemetry": "Telemetrie",
"telemetry_description": "Schakel anonieme gebruiksstatistieken in", "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", "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", "launch_with_system": "Start Hydra bij het opstarten van het systeem",
"general": "Algemeen", "general": "Algemeen",
"behavior": "Gedrag", "behavior": "Gedrag",
"enable_real_debrid": "Enable Real Debrid", "enable_real_debrid": "Enable Real-Debrid",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"real_debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.", "real_debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
"save_changes": "Wijzigingen opslaan" "save_changes": "Wijzigingen opslaan"
}, },

View File

@ -134,7 +134,7 @@
"delete_modal_title": "Czy na pewno?", "delete_modal_title": "Czy na pewno?",
"delete_modal_description": "Spowoduje to usunięcie wszystkich plików instalacyjnych z komputera", "delete_modal_description": "Spowoduje to usunięcie wszystkich plików instalacyjnych z komputera",
"install": "Instaluj", "install": "Instaluj",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"torrent": "Torrent" "torrent": "Torrent"
}, },
"settings": { "settings": {
@ -145,13 +145,13 @@
"enable_repack_list_notifications": "Gdy dodawany jest nowy repack", "enable_repack_list_notifications": "Gdy dodawany jest nowy repack",
"telemetry": "Telemetria", "telemetry": "Telemetria",
"telemetry_description": "Włącz anonimowe statystyki użycia", "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", "quit_app_instead_hiding": "Zamknij Hydr zamiast minimalizować do zasobnika",
"launch_with_system": "Uruchom Hydra przy starcie systemu", "launch_with_system": "Uruchom Hydra przy starcie systemu",
"general": "Ogólne", "general": "Ogólne",
"behavior": "Zachowania", "behavior": "Zachowania",
"enable_real_debrid": "Włącz Real Debrid", "enable_real_debrid": "Włącz Real-Debrid",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"real_debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>.", "real_debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>.",
"save_changes": "Zapisz zmiany" "save_changes": "Zapisz zmiany"
}, },

View File

@ -131,7 +131,7 @@
"deleting": "Excluindo instalador…", "deleting": "Excluindo instalador…",
"install": "Instalar", "install": "Instalar",
"torrent": "Torrent", "torrent": "Torrent",
"real_debrid": "Real Debrid" "real_debrid": "Real-Debrid"
}, },
"settings": { "settings": {
"downloads_path": "Diretório dos downloads", "downloads_path": "Diretório dos downloads",
@ -141,13 +141,13 @@
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada", "enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
"telemetry": "Telemetria", "telemetry": "Telemetria",
"telemetry_description": "Habilitar estatísticas de uso anônimas", "telemetry_description": "Habilitar estatísticas de uso anônimas",
"real_debrid_api_token_label": "Token de API do Real Debrid", "real_debrid_api_token_label": "Token de API do Real-Debrid",
"quit_app_instead_hiding": "Fechar o aplicativo em vez de minimizá-lo", "quit_app_instead_hiding": "Fechar o aplicativo em vez de minimizá-lo",
"launch_with_system": "Iniciar aplicativo na inicialização do sistema", "launch_with_system": "Iniciar aplicativo na inicialização do sistema",
"general": "Geral", "general": "Geral",
"behavior": "Comportamento", "behavior": "Comportamento",
"enable_real_debrid": "Habilitar Real Debrid", "enable_real_debrid": "Habilitar Real-Debrid",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"real_debrid_api_token_hint": "Você pode obter sua chave de API <0>aqui</0>.", "real_debrid_api_token_hint": "Você pode obter sua chave de API <0>aqui</0>.",
"save_changes": "Salvar mudanças" "save_changes": "Salvar mudanças"
}, },

View File

@ -134,7 +134,7 @@
"delete_modal_title": "Вы уверены?", "delete_modal_title": "Вы уверены?",
"delete_modal_description": "Это удалит все установщики с вашего компьютера", "delete_modal_description": "Это удалит все установщики с вашего компьютера",
"install": "Установить", "install": "Установить",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"torrent": "Torrent" "torrent": "Torrent"
}, },
"settings": { "settings": {
@ -145,13 +145,13 @@
"enable_repack_list_notifications": "При добавлении нового репака", "enable_repack_list_notifications": "При добавлении нового репака",
"telemetry": "Телеметрия", "telemetry": "Телеметрия",
"telemetry_description": "Отправлять анонимную статистику использования", "telemetry_description": "Отправлять анонимную статистику использования",
"real_debrid_api_token_label": "Real Debrid API-токен", "real_debrid_api_token_label": "Real-Debrid API-токен",
"quit_app_instead_hiding": "Закрывать Hydra вместо того, чтобы сворачивать его в трей", "quit_app_instead_hiding": "Закрывать Hydra вместо того, чтобы сворачивать его в трей",
"launch_with_system": "Запуск Hydra вместе с системой", "launch_with_system": "Запуск Hydra вместе с системой",
"general": "Основные", "general": "Основные",
"behavior": "Поведение", "behavior": "Поведение",
"enable_real_debrid": "Включить Real Debrid", "enable_real_debrid": "Включить Real-Debrid",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"real_debrid_api_token_hint": "API ключ можно получить <0>здесь</0>.", "real_debrid_api_token_hint": "API ключ можно получить <0>здесь</0>.",
"save_changes": "Сохранить изменения" "save_changes": "Сохранить изменения"
}, },

View File

@ -132,7 +132,7 @@
"delete_modal_title": "您确定吗?", "delete_modal_title": "您确定吗?",
"delete_modal_description": "这将从您的电脑上移除所有的安装文件", "delete_modal_description": "这将从您的电脑上移除所有的安装文件",
"install": "安装", "install": "安装",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"torrent": "种子" "torrent": "种子"
}, },
"settings": { "settings": {
@ -143,13 +143,13 @@
"enable_repack_list_notifications": "添加新重打包时", "enable_repack_list_notifications": "添加新重打包时",
"telemetry": "遥测", "telemetry": "遥测",
"telemetry_description": "启用匿名使用统计", "telemetry_description": "启用匿名使用统计",
"real_debrid_api_token_description": "Real Debrid API密钥", "real_debrid_api_token_description": "Real-Debrid API密钥",
"behavior": "行为", "behavior": "行为",
"general": "常规", "general": "常规",
"quit_app_instead_hiding": "关闭应用程序而不是最小化到托盘", "quit_app_instead_hiding": "关闭应用程序而不是最小化到托盘",
"launch_with_system": "随系统启动时运行应用程序", "launch_with_system": "随系统启动时运行应用程序",
"enable_real_debrid": "启用 Real Debrid", "enable_real_debrid": "启用 Real-Debrid",
"real_debrid": "Real Debrid", "real_debrid": "Real-Debrid",
"real_debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.", "real_debrid_api_token_hint": "您可以从<0>这里</0>获取API密钥.",
"save_changes": "保存更改" "save_changes": "保存更改"
}, },

View File

@ -29,7 +29,7 @@ const resumeGameDownload = async (
.getRepository(Game) .getRepository(Game)
.update({ status: "active", progress: Not(1) }, { status: "paused" }); .update({ status: "active", progress: Not(1) }, { status: "paused" });
await DownloadManager.resumeDownload(gameId); await DownloadManager.resumeDownload(game);
await transactionalEntityManager await transactionalEntityManager
.getRepository(Game) .getRepository(Game)

View File

@ -19,6 +19,7 @@ const startGameDownload = async (
where: { where: {
objectID, objectID,
}, },
relations: { repack: true },
}), }),
repackRepository.findOne({ repackRepository.findOne({
where: { where: {
@ -50,9 +51,7 @@ const startGameDownload = async (
} }
); );
await DownloadManager.startDownload(game.id); return DownloadManager.startDownload(game);
return { ...game, stauts: "active" };
} else { } else {
const steamGame = stateManager const steamGame = stateManager
.getValue("steamGames") .getValue("steamGames")
@ -62,8 +61,8 @@ const startGameDownload = async (
? getSteamAppAsset("icon", objectID, steamGame.clientIcon) ? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
: null; : null;
const createdGame = await gameRepository await gameRepository
.save({ .insert({
title, title,
iconUrl, iconUrl,
objectID, objectID,
@ -83,9 +82,14 @@ const startGameDownload = async (
return result; return result;
}); });
await DownloadManager.startDownload(createdGame.id); const createdGame = await gameRepository.findOne({
where: {
objectID,
},
relations: { repack: true },
});
return createdGame; return DownloadManager.startDownload(createdGame!);
} }
}; };

View File

@ -2,23 +2,17 @@ import { userPreferencesRepository } from "@main/repository";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import type { UserPreferences } from "@types"; import type { UserPreferences } from "@types";
import { RealDebridClient } from "@main/services/real-debrid";
const updateUserPreferences = async ( const updateUserPreferences = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
preferences: Partial<UserPreferences> preferences: Partial<UserPreferences>
) => { ) =>
if (preferences.realDebridApiToken) { userPreferencesRepository.upsert(
RealDebridClient.authorize(preferences.realDebridApiToken);
}
await userPreferencesRepository.upsert(
{ {
id: 1, id: 1,
...preferences, ...preferences,
}, },
["id"] ["id"]
); );
};
registerEvent("updateUserPreferences", updateUserPreferences); registerEvent("updateUserPreferences", updateUserPreferences);

View File

@ -97,7 +97,7 @@ const loadState = async (userPreferences: UserPreferences | null) => {
relations: { repack: true }, relations: { repack: true },
}); });
if (game) DownloadManager.startDownload(game.id); if (game) DownloadManager.startDownload(game);
}; };
userPreferencesRepository userPreferencesRepository

View File

@ -0,0 +1,27 @@
import path from "node:path";
import { spawn } from "node:child_process";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { app } from "electron";
export const startAria2 = (): Promise<ChildProcessWithoutNullStreams> => {
return new Promise((resolve) => {
const binaryPath = app.isPackaged
? path.join(process.resourcesPath, "aria2", "aria2c")
: path.join(__dirname, "..", "..", "aria2", "aria2c");
const cp = spawn(binaryPath, [
"--enable-rpc",
"--rpc-listen-all",
"--file-allocation=none",
"--allow-overwrite=true",
]);
cp.stdout.on("data", async (data) => {
const msg = Buffer.from(data).toString("utf-8");
if (msg.includes("IPv6 RPC: listening on TCP")) {
resolve(cp);
}
});
});
};

View File

@ -1,57 +1,39 @@
import Aria2, { StatusResponse } from "aria2"; import Aria2, { StatusResponse } from "aria2";
import { spawn } from "node:child_process";
import { gameRepository, userPreferencesRepository } from "@main/repository"; import { gameRepository, userPreferencesRepository } from "@main/repository";
import path from "node:path";
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { RealDebridClient } from "./real-debrid"; import { RealDebridClient } from "./real-debrid";
import { Notification, app } from "electron"; import { Notification } from "electron";
import { t } from "i18next"; import { t } from "i18next";
import { Downloader } from "@shared"; import { Downloader } from "@shared";
import { DownloadProgress } from "@types"; import { DownloadProgress } from "@types";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { Game } from "@main/entity"; import { Game } from "@main/entity";
import { startAria2 } from "./aria2";
export class DownloadManager { export class DownloadManager {
private static downloads = new Map<number, string>(); private static downloads = new Map<number, string>();
private static connected = false; private static connected = false;
private static gid: string | null = null; private static gid: string | null = null;
private static gameId: number | null = null; private static game: Game | null = null;
private static realDebridTorrentId: string | null = null;
private static aria2 = new Aria2({}); private static aria2 = new Aria2({});
private static connect(): Promise<boolean> { private static async connect() {
return new Promise((resolve) => { await startAria2();
const binaryPath = app.isPackaged await this.aria2.open();
? path.join(process.resourcesPath, "aria2", "aria2c") this.connected = true;
: path.join(__dirname, "..", "..", "aria2", "aria2c");
const cp = spawn(binaryPath, [
"--enable-rpc",
"--rpc-listen-all",
"--file-allocation=none",
"--allow-overwrite=true",
]);
cp.stdout.on("data", async (data) => {
const msg = Buffer.from(data).toString("utf-8");
if (msg.includes("IPv6 RPC: listening on TCP")) {
await this.aria2.open();
this.connected = true;
resolve(true);
}
});
});
} }
private static getETA(status: StatusResponse) { private static getETA(
const remainingBytes = totalLength: number,
Number(status.totalLength) - Number(status.completedLength); completedLength: number,
const speed = Number(status.downloadSpeed); speed: number
) {
const remainingBytes = totalLength - completedLength;
if (remainingBytes >= 0 && speed > 0) { if (remainingBytes >= 0 && speed > 0) {
return (remainingBytes / speed) * 1000; return (remainingBytes / speed) * 1000;
@ -65,9 +47,7 @@ export class DownloadManager {
where: { id: 1 }, where: { id: 1 },
}); });
if (userPreferences?.downloadNotificationsEnabled && this.gameId) { if (userPreferences?.downloadNotificationsEnabled && this.game) {
const game = await this.getGame(this.gameId);
new Notification({ new Notification({
title: t("download_complete", { title: t("download_complete", {
ns: "notifications", ns: "notifications",
@ -76,7 +56,7 @@ export class DownloadManager {
body: t("game_ready_to_install", { body: t("game_ready_to_install", {
ns: "notifications", ns: "notifications",
lng: userPreferences.language, lng: userPreferences.language,
title: game?.title, title: this.game.title,
}), }),
}).show(); }).show();
} }
@ -87,8 +67,73 @@ export class DownloadManager {
return ""; 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() { public static async watchDownloads() {
if (!this.gid || !this.gameId) return; if (!this.game) return;
if (!this.gid && this.realDebridTorrentId) {
const options = { dir: this.game.downloadPath! };
const downloadUrl = await this.getRealDebridDownloadUrl();
if (downloadUrl) {
this.gid = await this.aria2.call("addUri", [downloadUrl], options);
this.downloads.set(this.game.id, this.gid);
this.realDebridTorrentId = null;
}
}
if (!this.gid) return;
const status = await this.aria2.call("tellStatus", this.gid); const status = await this.aria2.call("tellStatus", this.gid);
@ -96,7 +141,7 @@ export class DownloadManager {
if (status.followedBy?.length) { if (status.followedBy?.length) {
this.gid = status.followedBy[0]; this.gid = status.followedBy[0];
this.downloads.set(this.gameId, this.gid); this.downloads.set(this.game.id, this.gid);
return; return;
} }
@ -113,7 +158,7 @@ export class DownloadManager {
if (!isNaN(progress)) update.progress = progress; if (!isNaN(progress)) update.progress = progress;
await gameRepository.update( await gameRepository.update(
{ id: this.gameId }, { id: this.game.id },
{ {
...update, ...update,
status: status.status, status: status.status,
@ -123,17 +168,18 @@ export class DownloadManager {
} }
const game = await gameRepository.findOne({ const game = await gameRepository.findOne({
where: { id: this.gameId, isDeleted: false }, where: { id: this.game.id, isDeleted: false },
relations: { repack: true }, relations: { repack: true },
}); });
if (progress === 1 && game && !isDownloadingMetadata) { if (progress === 1 && this.game && !isDownloadingMetadata) {
await this.publishNotification(); await this.publishNotification();
/* /*
Only cancel bittorrent downloads to stop seeding Only cancel bittorrent downloads to stop seeding
*/ */
if (status.bittorrent) { if (status.bittorrent) {
await this.cancelDownload(game.id); await this.cancelDownload(this.game.id);
} else { } else {
this.clearCurrentDownload(); this.clearCurrentDownload();
} }
@ -143,13 +189,14 @@ export class DownloadManager {
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
const payload = { const payload = {
progress,
bytesDownloaded: Number(status.completedLength),
fileSize: Number(status.totalLength),
numPeers: Number(status.connections), numPeers: Number(status.connections),
numSeeds: Number(status.numSeeders ?? 0), numSeeds: Number(status.numSeeders ?? 0),
downloadSpeed: Number(status.downloadSpeed), downloadSpeed: Number(status.downloadSpeed),
timeRemaining: this.getETA(status), timeRemaining: this.getETA(
Number(status.totalLength),
Number(status.completedLength),
Number(status.downloadSpeed)
),
isDownloadingMetadata: !!isDownloadingMetadata, isDownloadingMetadata: !!isDownloadingMetadata,
game, game,
} as DownloadProgress; } as DownloadProgress;
@ -161,20 +208,12 @@ export class DownloadManager {
} }
} }
static async getGame(gameId: number) {
return gameRepository.findOne({
where: { id: gameId, isDeleted: false },
relations: {
repack: true,
},
});
}
private static clearCurrentDownload() { private static clearCurrentDownload() {
if (this.gameId) { if (this.game) {
this.downloads.delete(this.gameId); this.downloads.delete(this.game.id);
this.gid = null; this.gid = null;
this.gameId = null; this.game = null;
this.realDebridTorrentId = null;
} }
} }
@ -198,50 +237,42 @@ export class DownloadManager {
if (this.gid) { if (this.gid) {
await this.aria2.call("forcePause", this.gid); await this.aria2.call("forcePause", this.gid);
this.gid = null; this.gid = null;
this.gameId = null; this.game = null;
this.realDebridTorrentId = null;
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
} }
} }
static async resumeDownload(gameId: number) { static async resumeDownload(game: Game) {
if (this.downloads.has(gameId)) { if (this.downloads.has(game.id)) {
const gid = this.downloads.get(gameId)!; const gid = this.downloads.get(game.id)!;
await this.aria2.call("unpause", gid); await this.aria2.call("unpause", gid);
this.gid = gid; this.gid = gid;
this.gameId = gameId; this.game = game;
} else { } else {
return this.startDownload(gameId); return this.startDownload(game);
} }
} }
static async startDownload(gameId: number) { static async startDownload(game: Game) {
if (!this.connected) await this.connect(); if (!this.connected) await this.connect();
const game = await this.getGame(gameId)!; const options = {
dir: game.downloadPath!,
};
if (game) { if (game.downloader === Downloader.RealDebrid) {
const options = { this.realDebridTorrentId = await RealDebridClient.getTorrentId(
dir: game.downloadPath!, game!.repack.magnet
}; );
} else {
this.gid = await this.aria2.call("addUri", [game.repack.magnet], options);
if (game.downloader === Downloader.RealDebrid) { this.downloads.set(game.id, this.gid);
const downloadUrl = decodeURIComponent(
await RealDebridClient.getDownloadUrl(game)
);
this.gid = await this.aria2.call("addUri", [downloadUrl], options);
} else {
this.gid = await this.aria2.call(
"addUri",
[game.repack.magnet],
options
);
}
this.gameId = gameId;
this.downloads.set(gameId, this.gid);
} }
this.game = game;
} }
} }

View File

@ -1,5 +1,5 @@
import { Game } from "@main/entity";
import axios, { AxiosInstance } from "axios"; import axios, { AxiosInstance } from "axios";
import parseTorrent from "parse-torrent";
import type { import type {
RealDebridAddMagnet, RealDebridAddMagnet,
RealDebridTorrentInfo, RealDebridTorrentInfo,
@ -7,10 +7,18 @@ import type {
RealDebridUser, RealDebridUser,
} from "@types"; } from "@types";
const base = "https://api.real-debrid.com/rest/1.0";
export class RealDebridClient { export class RealDebridClient {
private static instance: AxiosInstance; 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) { static async addMagnet(magnet: string) {
const searchParams = new URLSearchParams({ magnet }); const searchParams = new URLSearchParams({ magnet });
@ -23,7 +31,7 @@ export class RealDebridClient {
return response.data; return response.data;
} }
static async getInfo(id: string) { static async getTorrentInfo(id: string) {
const response = await this.instance.get<RealDebridTorrentInfo>( const response = await this.instance.get<RealDebridTorrentInfo>(
`/torrents/info/${id}` `/torrents/info/${id}`
); );
@ -55,50 +63,24 @@ export class RealDebridClient {
return response.data; return response.data;
} }
static async getAllTorrentsFromUser() { private static async getAllTorrentsFromUser() {
const response = const response =
await this.instance.get<RealDebridTorrentInfo[]>("/torrents"); await this.instance.get<RealDebridTorrentInfo[]>("/torrents");
return response.data; return response.data;
} }
static extractSHA1FromMagnet(magnet: string) { static async getTorrentId(magnetUri: string) {
return magnet.match(/btih:([0-9a-fA-F]*)/)?.[1].toLowerCase(); const userTorrents = await RealDebridClient.getAllTorrentsFromUser();
}
static async getDownloadUrl(game: Game) { const { infoHash } = await parseTorrent(magnetUri);
const torrents = await RealDebridClient.getAllTorrentsFromUser(); const userTorrent = userTorrents.find(
const hash = RealDebridClient.extractSHA1FromMagnet(game!.repack.magnet); (userTorrent) => userTorrent.hash === infoHash
let torrent = torrents.find((t) => t.hash === hash); );
// User haven't downloaded this torrent yet if (userTorrent) return userTorrent.id;
if (!torrent) {
const magnet = await RealDebridClient.addMagnet(game!.repack.magnet);
if (magnet) { const torrent = await RealDebridClient.addMagnet(magnetUri);
await RealDebridClient.selectAllFiles(magnet.id); return torrent.id;
torrent = await RealDebridClient.getInfo(magnet.id);
const { links } = torrent;
const { download } = await RealDebridClient.unrestrictLink(links[0]);
if (!download) {
throw new Error("Torrent not cached on Real Debrid");
}
return download;
}
}
throw new Error();
}
static authorize(apiToken: string) {
this.instance = axios.create({
baseURL: base,
headers: {
Authorization: `Bearer ${apiToken}`,
},
});
} }
} }

View File

@ -26,9 +26,9 @@ globalStyle("body", {
overflow: "hidden", overflow: "hidden",
userSelect: "none", userSelect: "none",
fontFamily: "'Fira Mono', monospace", fontFamily: "'Fira Mono', monospace",
fontSize: vars.size.bodyFontSize, fontSize: vars.size.body,
background: vars.color.background, background: vars.color.background,
color: vars.color.bodyText, color: vars.color.body,
margin: "0", margin: "0",
}); });
@ -68,7 +68,7 @@ globalStyle(
); );
globalStyle("label", { globalStyle("label", {
fontSize: vars.size.bodyFontSize, fontSize: vars.size.body,
}); });
globalStyle("input[type=number]", { globalStyle("input[type=number]", {

View File

@ -134,17 +134,18 @@ export function App() {
<section ref={contentRef} className={styles.content}> <section ref={contentRef} className={styles.content}>
<Outlet /> <Outlet />
<Toast
visible={toast.visible}
message={toast.message}
type={toast.type}
onClose={handleToastClose}
/>
</section> </section>
</article> </article>
</main> </main>
<BottomPanel /> <BottomPanel />
<Toast
visible={toast.visible}
message={toast.message}
type={toast.type}
onClose={handleToastClose}
/>
</> </>
); );
} }

View File

@ -4,6 +4,7 @@ import { SPACING_UNIT, vars } from "../../theme.css";
export const bottomPanel = style({ export const bottomPanel = style({
width: "100%", width: "100%",
borderTop: `solid 1px ${vars.color.border}`, borderTop: `solid 1px ${vars.color.border}`,
backgroundColor: vars.color.background,
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 2}px`,
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@ -14,10 +15,10 @@ export const bottomPanel = style({
}); });
export const downloadsButton = style({ export const downloadsButton = style({
color: vars.color.bodyText, color: vars.color.body,
borderBottom: "1px solid transparent", borderBottom: "1px solid transparent",
":hover": { ":hover": {
borderBottom: `1px solid ${vars.color.bodyText}`, borderBottom: `1px solid ${vars.color.body}`,
cursor: "pointer", cursor: "pointer",
}, },
}); });

View File

@ -109,6 +109,6 @@ export const shopIcon = style({
}); });
export const noDownloadsLabel = style({ export const noDownloadsLabel = style({
color: vars.color.bodyText, color: vars.color.body,
fontWeight: "bold", fontWeight: "bold",
}); });

View File

@ -108,7 +108,7 @@ export const section = style({
export const backButton = recipe({ export const backButton = recipe({
base: { base: {
color: vars.color.bodyText, color: vars.color.body,
cursor: "pointer", cursor: "pointer",
WebkitAppRegion: "no-drag", WebkitAppRegion: "no-drag",
position: "absolute", position: "absolute",

View File

@ -23,7 +23,7 @@ export const modal = recipe({
backgroundColor: vars.color.background, backgroundColor: vars.color.background,
borderRadius: "5px", borderRadius: "5px",
maxWidth: "600px", maxWidth: "600px",
color: vars.color.bodyText, color: vars.color.body,
maxHeight: "100%", maxHeight: "100%",
border: `solid 1px ${vars.color.border}`, border: `solid 1px ${vars.color.border}`,
overflow: "hidden", overflow: "hidden",
@ -65,5 +65,5 @@ export const closeModalButton = style({
}); });
export const closeModalButtonIcon = style({ export const closeModalButtonIcon = style({
color: vars.color.bodyText, color: vars.color.body,
}); });

View File

@ -25,12 +25,13 @@ export const toast = recipe({
borderRadius: "4px", borderRadius: "4px",
border: `solid 1px ${vars.color.border}`, border: `solid 1px ${vars.color.border}`,
left: "50%", left: "50%",
/* Bottom panel height + spacing */ /* Bottom panel height + 16px */
bottom: `${26 + SPACING_UNIT * 2}px`, bottom: `${26 + SPACING_UNIT * 2}px`,
overflow: "hidden", overflow: "hidden",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "space-between", justifyContent: "space-between",
zIndex: "0",
}, },
variants: { variants: {
closing: { closing: {
@ -66,7 +67,7 @@ export const progress = style({
}); });
export const closeButton = style({ export const closeButton = style({
color: vars.color.bodyText, color: vars.color.body,
cursor: "pointer", cursor: "pointer",
padding: "0", padding: "0",
margin: "0", margin: "0",

View File

@ -22,7 +22,7 @@ declare global {
interface Electron { interface Electron {
/* Torrenting */ /* Torrenting */
startGameDownload: (payload: StartGameDownloadPayload) => Promise<Game>; startGameDownload: (payload: StartGameDownloadPayload) => Promise<void>;
cancelGameDownload: (gameId: number) => Promise<void>; cancelGameDownload: (gameId: number) => Promise<void>;
pauseGameDownload: (gameId: number) => Promise<void>; pauseGameDownload: (gameId: number) => Promise<void>;
resumeGameDownload: (gameId: number) => Promise<void>; resumeGameDownload: (gameId: number) => Promise<void>;

View File

@ -19,7 +19,6 @@ export const toastSlice = createSlice({
initialState, initialState,
reducers: { reducers: {
showToast: (state, action: PayloadAction<Omit<ToastState, "visible">>) => { showToast: (state, action: PayloadAction<Omit<ToastState, "visible">>) => {
console.log(action.payload);
state.message = action.payload.message; state.message = action.payload.message;
state.visible = true; state.visible = true;
}, },

View File

@ -12,7 +12,7 @@ export const downloadTitleWrapper = style({
export const downloadTitle = style({ export const downloadTitle = style({
fontWeight: "bold", fontWeight: "bold",
cursor: "pointer", cursor: "pointer",
color: vars.color.bodyText, color: vars.color.body,
textAlign: "left", textAlign: "left",
fontSize: "16px", fontSize: "16px",
display: "block", display: "block",

View File

@ -174,5 +174,5 @@ globalStyle(`${description} img`, {
}); });
globalStyle(`${description} a`, { globalStyle(`${description} a`, {
color: vars.color.bodyText, color: vars.color.body,
}); });

View File

@ -13,6 +13,6 @@ export const repackButton = style({
flexDirection: "column", flexDirection: "column",
alignItems: "flex-start", alignItems: "flex-start",
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,
color: vars.color.bodyText, color: vars.color.body,
padding: `${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT * 2}px`,
}); });

View File

@ -15,7 +15,7 @@ export const downloadsPathField = style({
export const hintText = style({ export const hintText = style({
fontSize: "12px", fontSize: "12px",
color: vars.color.bodyText, color: vars.color.body,
}); });
export const downloaders = style({ export const downloaders = style({

View File

@ -125,7 +125,7 @@ export function SelectFolderModal({
{selectedDownloader === Downloader.RealDebrid && ( {selectedDownloader === Downloader.RealDebrid && (
<CheckCircleFillIcon /> <CheckCircleFillIcon />
)} )}
Real Debrid Real-Debrid
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -88,5 +88,5 @@ export const howLongToBeatCategorySkeleton = style({
globalStyle(`${requirementsDetails} a`, { globalStyle(`${requirementsDetails} a`, {
display: "flex", display: "flex",
color: vars.color.bodyText, color: vars.color.body,
}); });

View File

@ -7,3 +7,8 @@ export const form = style({
flexDirection: "column", flexDirection: "column",
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,
}); });
export const description = style({
fontFamily: "'Fira Sans', sans-serif",
marginBottom: `${SPACING_UNIT}px`,
});

View File

@ -52,8 +52,6 @@ export function SettingsRealDebrid({
form.realDebridApiToken! form.realDebridApiToken!
); );
console.log(user);
if (user.type === "premium") { if (user.type === "premium") {
dispatch( dispatch(
showToast({ showToast({
@ -61,18 +59,22 @@ export function SettingsRealDebrid({
type: "success", type: "success",
}) })
); );
updateUserPreferences({
realDebridApiToken: form.useRealDebrid
? form.realDebridApiToken
: null,
});
} }
} }
// updateUserPreferences({
// realDebridApiToken: form.useRealDebrid ? form.realDebridApiToken : null,
// });
}; };
const isButtonDisabled = form.useRealDebrid && !form.realDebridApiToken; const isButtonDisabled = form.useRealDebrid && !form.realDebridApiToken;
return ( return (
<form className={styles.form} onSubmit={handleFormSubmit}> <form className={styles.form} onSubmit={handleFormSubmit}>
<p className={styles.description}>{t("real_debrid_description")}</p>
<CheckboxField <CheckboxField
label={t("enable_real_debrid")} label={t("enable_real_debrid")}
checked={form.useRealDebrid} checked={form.useRealDebrid}
@ -86,7 +88,7 @@ export function SettingsRealDebrid({
{form.useRealDebrid && ( {form.useRealDebrid && (
<TextField <TextField
label={t("real_debrid_api_token_label")} label="API Private Token"
value={form.realDebridApiToken ?? ""} value={form.realDebridApiToken ?? ""}
type="password" type="password"
onChange={(event) => onChange={(event) =>

View File

@ -7,7 +7,7 @@ export const [themeClass, vars] = createTheme({
background: "#1c1c1c", background: "#1c1c1c",
darkBackground: "#151515", darkBackground: "#151515",
muted: "#c0c1c7", muted: "#c0c1c7",
bodyText: "#8e919b", body: "#8e919b",
border: "#424244", border: "#424244",
success: "#1c9749", success: "#1c9749",
danger: "#e11d48", danger: "#e11d48",
@ -17,6 +17,6 @@ export const [themeClass, vars] = createTheme({
active: "0.7", active: "0.7",
}, },
size: { size: {
bodyFontSize: "14px", body: "14px",
}, },
}); });

View File

@ -115,9 +115,6 @@ export interface DownloadProgress {
numPeers: number; numPeers: number;
numSeeds: number; numSeeds: number;
isDownloadingMetadata: boolean; isDownloadingMetadata: boolean;
progress: number;
bytesDownloaded: number;
fileSize: number;
game: Omit<Game, "repacks">; game: Omit<Game, "repacks">;
} }
@ -197,7 +194,18 @@ export interface RealDebridTorrentInfo {
host: string; host: string;
split: number; split: number;
progress: number; progress: number;
status: string; status:
| "magnet_error"
| "magnet_conversion"
| "waiting_files_selection"
| "queued"
| "downloading"
| "downloaded"
| "error"
| "virus"
| "compressing"
| "uploading"
| "dead";
added: string; added: string;
files: { files: {
id: number; id: number;