From 9e6143ebc90b6979887ca398395794c859a4cb70 Mon Sep 17 00:00:00 2001 From: mircea32000 <36380975+mircea32000@users.noreply.github.com> Date: Tue, 11 Feb 2025 00:58:47 +0200 Subject: [PATCH] Fix lints and add back the option to seed after download complete --- .../authenticate-all-debrid.ts | 8 +- .../update-user-preferences.ts | 4 +- src/main/main.ts | 11 +- src/main/services/download/all-debrid.ts | 511 ++++++++++-------- .../services/download/download-manager.ts | 90 +-- src/main/services/download/helpers.ts | 2 +- src/main/services/python-rpc.ts | 8 +- src/renderer/src/declaration.d.ts | 2 + .../game-details/hero/hero-panel-actions.tsx | 1 - .../modals/download-settings-modal.scss | 2 +- .../modals/download-settings-modal.tsx | 1 - .../pages/settings/settings-all-debrid.scss | 2 +- .../pages/settings/settings-all-debrid.tsx | 4 +- src/shared/index.ts | 7 +- src/types/download.types.ts | 2 +- 15 files changed, 360 insertions(+), 295 deletions(-) diff --git a/src/main/events/user-preferences/authenticate-all-debrid.ts b/src/main/events/user-preferences/authenticate-all-debrid.ts index 6e153fe5..d4bb36e8 100644 --- a/src/main/events/user-preferences/authenticate-all-debrid.ts +++ b/src/main/events/user-preferences/authenticate-all-debrid.ts @@ -7,12 +7,12 @@ const authenticateAllDebrid = async ( ) => { AllDebridClient.authorize(apiKey); const result = await AllDebridClient.getUser(); - - if ('error_code' in result) { + + if ("error_code" in result) { return { error_code: result.error_code }; } - + return result.user; }; -registerEvent("authenticateAllDebrid", authenticateAllDebrid); \ No newline at end of file +registerEvent("authenticateAllDebrid", authenticateAllDebrid); diff --git a/src/main/events/user-preferences/update-user-preferences.ts b/src/main/events/user-preferences/update-user-preferences.ts index 90a2d56c..c1598f29 100644 --- a/src/main/events/user-preferences/update-user-preferences.ts +++ b/src/main/events/user-preferences/update-user-preferences.ts @@ -31,9 +31,7 @@ const updateUserPreferences = async ( } if (preferences.allDebridApiKey) { - preferences.allDebridApiKey = Crypto.encrypt( - preferences.allDebridApiKey - ); + preferences.allDebridApiKey = Crypto.encrypt(preferences.allDebridApiKey); } if (preferences.torBoxApiToken) { diff --git a/src/main/main.ts b/src/main/main.ts index e09c473b..083c2548 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -45,15 +45,11 @@ export const loadState = async () => { } if (userPreferences?.allDebridApiKey) { - AllDebridClient.authorize( - Crypto.decrypt(userPreferences.allDebridApiKey) - ); + AllDebridClient.authorize(Crypto.decrypt(userPreferences.allDebridApiKey)); } if (userPreferences?.torBoxApiToken) { - TorBoxClient.authorize( - Crypto.decrypt(userPreferences.torBoxApiToken) - ); + TorBoxClient.authorize(Crypto.decrypt(userPreferences.torBoxApiToken)); } Ludusavi.addManifestToLudusaviConfig(); @@ -126,7 +122,8 @@ const migrateFromSqlite = async () => { .select("*") .then(async (userPreferences) => { if (userPreferences.length > 0) { - const { realDebridApiToken, allDebridApiKey, ...rest } = userPreferences[0]; + const { realDebridApiToken, allDebridApiKey, ...rest } = + userPreferences[0]; await db.put( levelKeys.userPreferences, diff --git a/src/main/services/download/all-debrid.ts b/src/main/services/download/all-debrid.ts index 63a67408..b1b04b7a 100644 --- a/src/main/services/download/all-debrid.ts +++ b/src/main/services/download/all-debrid.ts @@ -3,269 +3,310 @@ import type { AllDebridUser } from "@types"; import { logger } from "@main/services"; interface AllDebridMagnetStatus { - id: number; + id: number; + filename: string; + size: number; + status: string; + statusCode: number; + downloaded: number; + uploaded: number; + seeders: number; + downloadSpeed: number; + uploadSpeed: number; + uploadDate: number; + completionDate: number; + links: Array<{ + link: string; filename: string; size: number; - status: string; - statusCode: number; - downloaded: number; - uploaded: number; - seeders: number; - downloadSpeed: number; - uploadSpeed: number; - uploadDate: number; - completionDate: number; - links: Array<{ - link: string; - filename: string; - size: number; - }>; + }>; } interface AllDebridError { - code: string; - message: string; + code: string; + message: string; } interface AllDebridDownloadUrl { - link: string; - size?: number; - filename?: string; + link: string; + size?: number; + filename?: string; } export class AllDebridClient { - private static instance: AxiosInstance; - private static readonly baseURL = "https://api.alldebrid.com/v4"; + private static instance: AxiosInstance; + private static readonly baseURL = "https://api.alldebrid.com/v4"; - static authorize(apiKey: string) { - logger.info("[AllDebrid] Authorizing with key:", apiKey ? "***" : "empty"); - this.instance = axios.create({ - baseURL: this.baseURL, - params: { - agent: "hydra", - apikey: apiKey - } - }); - } + static authorize(apiKey: string) { + logger.info("[AllDebrid] Authorizing with key:", apiKey ? "***" : "empty"); + this.instance = axios.create({ + baseURL: this.baseURL, + params: { + agent: "hydra", + apikey: apiKey, + }, + }); + } - static async getUser() { - try { - const response = await this.instance.get<{ - status: string; - data?: { user: AllDebridUser }; - error?: AllDebridError; - }>("/user"); + static async getUser() { + try { + const response = await this.instance.get<{ + status: string; + data?: { user: AllDebridUser }; + error?: AllDebridError; + }>("/user"); - logger.info("[AllDebrid] API Response:", response.data); + logger.info("[AllDebrid] API Response:", response.data); - if (response.data.status === "error") { - const error = response.data.error; - logger.error("[AllDebrid] API Error:", error); - if (error?.code === "AUTH_MISSING_APIKEY") { - return { error_code: "alldebrid_missing_key" }; - } - if (error?.code === "AUTH_BAD_APIKEY") { - return { error_code: "alldebrid_invalid_key" }; - } - if (error?.code === "AUTH_BLOCKED") { - return { error_code: "alldebrid_blocked" }; - } - if (error?.code === "AUTH_USER_BANNED") { - return { error_code: "alldebrid_banned" }; - } - return { error_code: "alldebrid_unknown_error" }; - } - - if (!response.data.data?.user) { - logger.error("[AllDebrid] No user data in response"); - return { error_code: "alldebrid_invalid_response" }; - } - - logger.info("[AllDebrid] Successfully got user:", response.data.data.user.username); - return { user: response.data.data.user }; - } catch (error: any) { - logger.error("[AllDebrid] Request Error:", error); - if (error.response?.data?.error) { - return { error_code: "alldebrid_invalid_key" }; - } - return { error_code: "alldebrid_network_error" }; + if (response.data.status === "error") { + const error = response.data.error; + logger.error("[AllDebrid] API Error:", error); + if (error?.code === "AUTH_MISSING_APIKEY") { + return { error_code: "alldebrid_missing_key" }; } - } - - private static async uploadMagnet(magnet: string) { - try { - logger.info("[AllDebrid] Uploading magnet with params:", { magnet }); - - const response = await this.instance.get("/magnet/upload", { - params: { - magnets: [magnet] - } - }); - - logger.info("[AllDebrid] Upload Magnet Raw Response:", JSON.stringify(response.data, null, 2)); - - if (response.data.status === "error") { - throw new Error(response.data.error?.message || "Unknown error"); - } - - const magnetInfo = response.data.data.magnets[0]; - logger.info("[AllDebrid] Magnet Info:", JSON.stringify(magnetInfo, null, 2)); - - if (magnetInfo.error) { - throw new Error(magnetInfo.error.message); - } - - return magnetInfo.id; - } catch (error: any) { - logger.error("[AllDebrid] Upload Magnet Error:", error); - throw error; + if (error?.code === "AUTH_BAD_APIKEY") { + return { error_code: "alldebrid_invalid_key" }; } + if (error?.code === "AUTH_BLOCKED") { + return { error_code: "alldebrid_blocked" }; + } + if (error?.code === "AUTH_USER_BANNED") { + return { error_code: "alldebrid_banned" }; + } + return { error_code: "alldebrid_unknown_error" }; + } + + if (!response.data.data?.user) { + logger.error("[AllDebrid] No user data in response"); + return { error_code: "alldebrid_invalid_response" }; + } + + logger.info( + "[AllDebrid] Successfully got user:", + response.data.data.user.username + ); + return { user: response.data.data.user }; + } catch (error: any) { + logger.error("[AllDebrid] Request Error:", error); + if (error.response?.data?.error) { + return { error_code: "alldebrid_invalid_key" }; + } + return { error_code: "alldebrid_network_error" }; } + } - private static async checkMagnetStatus(magnetId: number): Promise { - try { - logger.info("[AllDebrid] Checking magnet status for ID:", magnetId); - - const response = await this.instance.get(`/magnet/status`, { - params: { - id: magnetId - } - }); + private static async uploadMagnet(magnet: string) { + try { + logger.info("[AllDebrid] Uploading magnet with params:", { magnet }); - logger.info("[AllDebrid] Check Magnet Status Raw Response:", JSON.stringify(response.data, null, 2)); + const response = await this.instance.get("/magnet/upload", { + params: { + magnets: [magnet], + }, + }); - if (!response.data) { - throw new Error("No response data received"); - } + logger.info( + "[AllDebrid] Upload Magnet Raw Response:", + JSON.stringify(response.data, null, 2) + ); - if (response.data.status === "error") { - throw new Error(response.data.error?.message || "Unknown error"); - } + if (response.data.status === "error") { + throw new Error(response.data.error?.message || "Unknown error"); + } - // Verificăm noua structură a răspunsului - const magnetData = response.data.data?.magnets; - if (!magnetData || typeof magnetData !== 'object') { - logger.error("[AllDebrid] Invalid response structure:", JSON.stringify(response.data, null, 2)); - throw new Error("Invalid magnet status response format"); - } + const magnetInfo = response.data.data.magnets[0]; + logger.info( + "[AllDebrid] Magnet Info:", + JSON.stringify(magnetInfo, null, 2) + ); - // Convertim răspunsul în formatul așteptat - const magnetStatus: AllDebridMagnetStatus = { - id: magnetData.id, - filename: magnetData.filename, - size: magnetData.size, - status: magnetData.status, - statusCode: magnetData.statusCode, - downloaded: magnetData.downloaded, - uploaded: magnetData.uploaded, - seeders: magnetData.seeders, - downloadSpeed: magnetData.downloadSpeed, - uploadSpeed: magnetData.uploadSpeed, - uploadDate: magnetData.uploadDate, - completionDate: magnetData.completionDate, - links: magnetData.links.map(link => ({ - link: link.link, + if (magnetInfo.error) { + throw new Error(magnetInfo.error.message); + } + + return magnetInfo.id; + } catch (error: any) { + logger.error("[AllDebrid] Upload Magnet Error:", error); + throw error; + } + } + + private static async checkMagnetStatus( + magnetId: number + ): Promise { + try { + logger.info("[AllDebrid] Checking magnet status for ID:", magnetId); + + const response = await this.instance.get(`/magnet/status`, { + params: { + id: magnetId, + }, + }); + + logger.info( + "[AllDebrid] Check Magnet Status Raw Response:", + JSON.stringify(response.data, null, 2) + ); + + if (!response.data) { + throw new Error("No response data received"); + } + + if (response.data.status === "error") { + throw new Error(response.data.error?.message || "Unknown error"); + } + + // Verificăm noua structură a răspunsului + const magnetData = response.data.data?.magnets; + if (!magnetData || typeof magnetData !== "object") { + logger.error( + "[AllDebrid] Invalid response structure:", + JSON.stringify(response.data, null, 2) + ); + throw new Error("Invalid magnet status response format"); + } + + // Convertim răspunsul în formatul așteptat + const magnetStatus: AllDebridMagnetStatus = { + id: magnetData.id, + filename: magnetData.filename, + size: magnetData.size, + status: magnetData.status, + statusCode: magnetData.statusCode, + downloaded: magnetData.downloaded, + uploaded: magnetData.uploaded, + seeders: magnetData.seeders, + downloadSpeed: magnetData.downloadSpeed, + uploadSpeed: magnetData.uploadSpeed, + uploadDate: magnetData.uploadDate, + completionDate: magnetData.completionDate, + links: magnetData.links.map((link) => ({ + link: link.link, + filename: link.filename, + size: link.size, + })), + }; + + logger.info( + "[AllDebrid] Magnet Status:", + JSON.stringify(magnetStatus, null, 2) + ); + + return magnetStatus; + } catch (error: any) { + logger.error("[AllDebrid] Check Magnet Status Error:", error); + throw error; + } + } + + private static async unlockLink(link: string) { + try { + const response = await this.instance.get<{ + status: string; + data?: { link: string }; + error?: AllDebridError; + }>("/link/unlock", { + params: { + link, + }, + }); + + if (response.data.status === "error") { + throw new Error(response.data.error?.message || "Unknown error"); + } + + const unlockedLink = response.data.data?.link; + if (!unlockedLink) { + throw new Error("No download link received from AllDebrid"); + } + + return unlockedLink; + } catch (error: any) { + logger.error("[AllDebrid] Unlock Link Error:", error); + throw error; + } + } + + public static async getDownloadUrls( + uri: string + ): Promise { + try { + logger.info("[AllDebrid] Getting download URLs for URI:", uri); + + if (uri.startsWith("magnet:")) { + logger.info("[AllDebrid] Detected magnet link, uploading..."); + // 1. Upload magnet + const magnetId = await this.uploadMagnet(uri); + logger.info("[AllDebrid] Magnet uploaded, ID:", magnetId); + + // 2. Verificăm statusul până când avem link-uri + let retries = 0; + let magnetStatus: AllDebridMagnetStatus; + + do { + magnetStatus = await this.checkMagnetStatus(magnetId); + logger.info( + "[AllDebrid] Magnet status:", + magnetStatus.status, + "statusCode:", + magnetStatus.statusCode + ); + + if (magnetStatus.statusCode === 4) { + // Ready + // Deblocăm fiecare link în parte și aruncăm eroare dacă oricare eșuează + const unlockedLinks = await Promise.all( + magnetStatus.links.map(async (link) => { + try { + const unlockedLink = await this.unlockLink(link.link); + logger.info( + "[AllDebrid] Successfully unlocked link:", + unlockedLink + ); + return { + link: unlockedLink, + size: link.size, filename: link.filename, - size: link.size - })) - }; - - logger.info("[AllDebrid] Magnet Status:", JSON.stringify(magnetStatus, null, 2)); - - return magnetStatus; - } catch (error: any) { - logger.error("[AllDebrid] Check Magnet Status Error:", error); - throw error; - } - } - - private static async unlockLink(link: string) { - try { - const response = await this.instance.get<{ - status: string; - data?: { link: string }; - error?: AllDebridError; - }>("/link/unlock", { - params: { - link + }; + } catch (error) { + logger.error( + "[AllDebrid] Failed to unlock link:", + link.link, + error + ); + throw new Error("Failed to unlock all links"); } - }); + }) + ); - if (response.data.status === "error") { - throw new Error(response.data.error?.message || "Unknown error"); - } + logger.info( + "[AllDebrid] Got unlocked download links:", + unlockedLinks + ); + return unlockedLinks; + } - const unlockedLink = response.data.data?.link; - if (!unlockedLink) { - throw new Error("No download link received from AllDebrid"); - } + if (retries++ > 30) { + // Maximum 30 de încercări + throw new Error("Timeout waiting for magnet to be ready"); + } - return unlockedLink; - } catch (error: any) { - logger.error("[AllDebrid] Unlock Link Error:", error); - throw error; - } - } - - public static async getDownloadUrls(uri: string): Promise { - try { - logger.info("[AllDebrid] Getting download URLs for URI:", uri); - - if (uri.startsWith("magnet:")) { - logger.info("[AllDebrid] Detected magnet link, uploading..."); - // 1. Upload magnet - const magnetId = await this.uploadMagnet(uri); - logger.info("[AllDebrid] Magnet uploaded, ID:", magnetId); - - // 2. Verificăm statusul până când avem link-uri - let retries = 0; - let magnetStatus: AllDebridMagnetStatus; - - do { - magnetStatus = await this.checkMagnetStatus(magnetId); - logger.info("[AllDebrid] Magnet status:", magnetStatus.status, "statusCode:", magnetStatus.statusCode); - - if (magnetStatus.statusCode === 4) { // Ready - // Deblocăm fiecare link în parte și aruncăm eroare dacă oricare eșuează - const unlockedLinks = await Promise.all( - magnetStatus.links.map(async link => { - try { - const unlockedLink = await this.unlockLink(link.link); - logger.info("[AllDebrid] Successfully unlocked link:", unlockedLink); - return { - link: unlockedLink, - size: link.size, - filename: link.filename - }; - } catch (error) { - logger.error("[AllDebrid] Failed to unlock link:", link.link, error); - throw new Error("Failed to unlock all links"); - } - }) - ); - - logger.info("[AllDebrid] Got unlocked download links:", unlockedLinks); - return unlockedLinks; - } - - if (retries++ > 30) { // Maximum 30 de încercări - throw new Error("Timeout waiting for magnet to be ready"); - } - - await new Promise(resolve => setTimeout(resolve, 2000)); // Așteptăm 2 secunde între verificări - } while (true); - } else { - logger.info("[AllDebrid] Regular link, unlocking..."); - // Pentru link-uri normale, doar debridam link-ul - const downloadUrl = await this.unlockLink(uri); - logger.info("[AllDebrid] Got unlocked download URL:", downloadUrl); - return [{ - link: downloadUrl - }]; - } - } catch (error: any) { - logger.error("[AllDebrid] Get Download URLs Error:", error); - throw error; - } + await new Promise((resolve) => setTimeout(resolve, 2000)); // Așteptăm 2 secunde între verificări + } while (true); + } else { + logger.info("[AllDebrid] Regular link, unlocking..."); + // Pentru link-uri normale, doar debridam link-ul + const downloadUrl = await this.unlockLink(uri); + logger.info("[AllDebrid] Got unlocked download URL:", downloadUrl); + return [ + { + link: downloadUrl, + }, + ]; + } + } catch (error: any) { + logger.error("[AllDebrid] Get Download URLs Error:", error); + throw error; } + } } diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index b4109b7b..c4ccc430 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -17,17 +17,6 @@ import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; import { sortBy } from "lodash-es"; import { TorBoxClient } from "./torbox"; import { AllDebridClient } from "./all-debrid"; -import { spawn } from "child_process"; - -interface GamePayload { - action: string; - game_id: string; - url: string | string[]; - save_path: string; - header?: string; - out?: string; - total_size?: number; -} export class DownloadManager { private static downloadingGameId: string | null = null; @@ -44,6 +33,7 @@ export class DownloadManager { }) : undefined, downloadsToSeed?.map((download) => ({ + action: "seed", game_id: levelKeys.game(download.shop, download.objectId), url: download.uri, save_path: download.downloadPath, @@ -145,15 +135,45 @@ export class DownloadManager { if (progress === 1 && download) { publishDownloadCompleteNotification(game); - await downloadsSublevel.put(gameId, { - ...download, - status: "complete", - shouldSeed: false, - queued: false, - }); + if ( + userPreferences?.seedAfterDownloadComplete && + download.downloader === Downloader.Torrent + ) { + downloadsSublevel.put(gameId, { + ...download, + status: "seeding", + shouldSeed: true, + queued: false, + }); + } else { + downloadsSublevel.put(gameId, { + ...download, + status: "complete", + shouldSeed: false, + queued: false, + }); - await this.cancelDownload(gameId); - this.downloadingGameId = null; + this.cancelDownload(gameId); + } + + const downloads = await downloadsSublevel + .values() + .all() + .then((games) => { + return sortBy( + games.filter((game) => game.status === "paused" && game.queued), + "timestamp", + "DESC" + ); + }); + + const [nextItemOnQueue] = downloads; + + if (nextItemOnQueue) { + this.resumeDownload(nextItemOnQueue); + } else { + this.downloadingGameId = null; + } } } } @@ -296,6 +316,21 @@ export class DownloadManager { save_path: download.downloadPath, }; } + case Downloader.AllDebrid: { + const downloadUrls = await AllDebridClient.getDownloadUrls(download.uri); + + if (!downloadUrls.length) throw new Error(DownloadError.NotCachedInAllDebrid); + + const totalSize = downloadUrls.reduce((total, url) => total + (url.size || 0), 0); + + return { + action: "start", + game_id: downloadId, + url: downloadUrls.map(d => d.link), + save_path: download.downloadPath, + total_size: totalSize + }; + } case Downloader.Torrent: return { action: "start", @@ -315,21 +350,6 @@ export class DownloadManager { save_path: download.downloadPath, }; } - case Downloader.AllDebrid: { - const downloadUrls = await AllDebridClient.getDownloadUrls(download.uri); - - if (!downloadUrls.length) throw new Error(DownloadError.NotCachedInAllDebrid); - - const totalSize = downloadUrls.reduce((total, url) => total + (url.size || 0), 0); - - return { - action: "start", - game_id: downloadId, - url: downloadUrls.map(d => d.link), - save_path: download.downloadPath, - total_size: totalSize - }; - } case Downloader.TorBox: { const { name, url } = await TorBoxClient.getDownloadInfo(download.uri); @@ -350,4 +370,4 @@ export class DownloadManager { await PythonRPC.rpc.post("/action", payload); this.downloadingGameId = levelKeys.game(download.shop, download.objectId); } -} +} \ No newline at end of file diff --git a/src/main/services/download/helpers.ts b/src/main/services/download/helpers.ts index ae039adf..84db662e 100644 --- a/src/main/services/download/helpers.ts +++ b/src/main/services/download/helpers.ts @@ -19,7 +19,7 @@ export const calculateETA = ( export const getDirSize = async (dir: string): Promise => { try { const stat = await fs.promises.stat(dir); - + // If it's a file, return its size directly if (!stat.isDirectory()) { return stat.size; diff --git a/src/main/services/python-rpc.ts b/src/main/services/python-rpc.ts index dcbca281..7ed22ed6 100644 --- a/src/main/services/python-rpc.ts +++ b/src/main/services/python-rpc.ts @@ -10,9 +10,13 @@ import { Readable } from "node:stream"; import { app, dialog } from "electron"; interface GamePayload { + action: string; game_id: string; - url: string; + url: string | string[]; save_path: string; + header?: string; + out?: string; + total_size?: number; } const binaryNameByPlatform: Partial> = { @@ -105,4 +109,4 @@ export class PythonRPC { this.pythonProcess = null; } } -} \ No newline at end of file +} diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 8e31aa83..666ba121 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -29,6 +29,7 @@ import type { LibraryGame, GameRunning, TorBoxUser, + AllDebridUser, } from "@types"; import type { AxiosProgressEvent } from "axios"; import type disk from "diskusage"; @@ -150,6 +151,7 @@ declare global { minimized: boolean; }) => Promise; authenticateRealDebrid: (apiToken: string) => Promise; + authenticateAllDebrid: (apiKey: string) => Promise; authenticateTorBox: (apiToken: string) => Promise; onAchievementUnlocked: (cb: () => void) => () => Electron.IpcRenderer; diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx index 37e0ff1f..9dc531f4 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx @@ -196,7 +196,6 @@ export function HeroPanelActions() { {game.favorite ? : } -