diff --git a/.env.example b/.env.example deleted file mode 100644 index 3ef399f7..00000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -MAIN_VITE_API_URL=API_URL -MAIN_VITE_AUTH_URL=AUTH_URL diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 94b52c75..c71f263b 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -306,7 +306,18 @@ "enable_torbox": "Enable Torbox", "torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.", "torbox_account_linked": "TorBox account linked", - "real_debrid_account_linked": "Real-Debrid account linked" + "real_debrid_account_linked": "Real-Debrid account linked", + "enable_all_debrid": "Enable All-Debrid", + "all_debrid_description": "All-Debrid is an unrestricted downloader that allows you to quickly download files from various sources.", + "all_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to All-Debrid", + "all_debrid_account_linked": "All-Debrid account linked successfully", + "alldebrid_missing_key": "Please provide an API key", + "alldebrid_invalid_key": "Invalid API key", + "alldebrid_blocked": "Your API key is geo-blocked or IP-blocked", + "alldebrid_banned": "This account has been banned", + "alldebrid_unknown_error": "An unknown error occurred", + "alldebrid_invalid_response": "Invalid response from All-Debrid", + "alldebrid_network_error": "Network error. Please check your connection" }, "notifications": { "download_complete": "Download complete", diff --git a/src/locales/ro/translation.json b/src/locales/ro/translation.json index 8a5403b5..0815cc88 100644 --- a/src/locales/ro/translation.json +++ b/src/locales/ro/translation.json @@ -134,7 +134,11 @@ "real_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la Real-Debrid", "debrid_linked_message": "Contul \"{{username}}\" a fost legat", "save_changes": "Salvează modificările", - "changes_saved": "Modificările au fost salvate cu succes" + "changes_saved": "Modificările au fost salvate cu succes", + "enable_all_debrid": "Activează All-Debrid", + "all_debrid_description": "All-Debrid este un descărcător fără restricții care îți permite să descarci fișiere din diverse surse.", + "all_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la All-Debrid", + "all_debrid_account_linked": "Contul All-Debrid a fost conectat cu succes" }, "notifications": { "download_complete": "Descărcare completă", diff --git a/src/main/events/index.ts b/src/main/events/index.ts index dc64b40e..b3eafdc1 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -48,6 +48,7 @@ import "./user-preferences/auto-launch"; import "./autoupdater/check-for-updates"; import "./autoupdater/restart-and-install-update"; import "./user-preferences/authenticate-real-debrid"; +import "./user-preferences/authenticate-all-debrid"; import "./user-preferences/authenticate-torbox"; import "./download-sources/put-download-source"; import "./auth/sign-out"; diff --git a/src/main/events/user-preferences/authenticate-all-debrid.ts b/src/main/events/user-preferences/authenticate-all-debrid.ts new file mode 100644 index 00000000..6e153fe5 --- /dev/null +++ b/src/main/events/user-preferences/authenticate-all-debrid.ts @@ -0,0 +1,18 @@ +import { AllDebridClient } from "@main/services/download/all-debrid"; +import { registerEvent } from "../register-event"; + +const authenticateAllDebrid = async ( + _event: Electron.IpcMainInvokeEvent, + apiKey: string +) => { + AllDebridClient.authorize(apiKey); + const result = await AllDebridClient.getUser(); + + if ('error_code' in result) { + return { error_code: result.error_code }; + } + + return result.user; +}; + +registerEvent("authenticateAllDebrid", authenticateAllDebrid); \ No newline at end of file diff --git a/src/main/events/user-preferences/get-user-preferences.ts b/src/main/events/user-preferences/get-user-preferences.ts index c67f72b9..5dd3d57c 100644 --- a/src/main/events/user-preferences/get-user-preferences.ts +++ b/src/main/events/user-preferences/get-user-preferences.ts @@ -15,6 +15,12 @@ const getUserPreferences = async () => ); } + if (userPreferences?.allDebridApiKey) { + userPreferences.allDebridApiKey = Crypto.decrypt( + userPreferences.allDebridApiKey + ); + } + if (userPreferences?.torBoxApiToken) { userPreferences.torBoxApiToken = Crypto.decrypt( userPreferences.torBoxApiToken diff --git a/src/main/events/user-preferences/update-user-preferences.ts b/src/main/events/user-preferences/update-user-preferences.ts index 275a6f27..90a2d56c 100644 --- a/src/main/events/user-preferences/update-user-preferences.ts +++ b/src/main/events/user-preferences/update-user-preferences.ts @@ -30,6 +30,12 @@ const updateUserPreferences = async ( ); } + if (preferences.allDebridApiKey) { + preferences.allDebridApiKey = Crypto.encrypt( + preferences.allDebridApiKey + ); + } + if (preferences.torBoxApiToken) { preferences.torBoxApiToken = Crypto.encrypt(preferences.torBoxApiToken); } diff --git a/src/main/main.ts b/src/main/main.ts index 4824a1a5..e09c473b 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -6,6 +6,7 @@ import { startMainLoop, } from "./services"; import { RealDebridClient } from "./services/download/real-debrid"; +import { AllDebridClient } from "./services/download/all-debrid"; import { HydraApi } from "./services/hydra-api"; import { uploadGamesBatch } from "./services/library-sync"; import { Aria2 } from "./services/aria2"; @@ -43,8 +44,16 @@ export const loadState = async () => { ); } + if (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(); @@ -117,7 +126,7 @@ const migrateFromSqlite = async () => { .select("*") .then(async (userPreferences) => { if (userPreferences.length > 0) { - const { realDebridApiToken, ...rest } = userPreferences[0]; + const { realDebridApiToken, allDebridApiKey, ...rest } = userPreferences[0]; await db.put( levelKeys.userPreferences, @@ -126,6 +135,9 @@ const migrateFromSqlite = async () => { realDebridApiToken: realDebridApiToken ? Crypto.encrypt(realDebridApiToken) : null, + allDebridApiKey: allDebridApiKey + ? Crypto.encrypt(allDebridApiKey) + : null, preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1, runAtStartup: rest.runAtStartup === 1, startMinimized: rest.startMinimized === 1, diff --git a/src/main/services/download/all-debrid.ts b/src/main/services/download/all-debrid.ts new file mode 100644 index 00000000..864710b8 --- /dev/null +++ b/src/main/services/download/all-debrid.ts @@ -0,0 +1,66 @@ +import axios, { AxiosInstance } from "axios"; +import type { AllDebridUser } from "@types"; +import { logger } from "@main/services"; + +export class AllDebridClient { + private static instance: AxiosInstance; + private static readonly baseURL = "https://api.alldebrid.com/v4"; + + static authorize(apiKey: string) { + logger.info("[AllDebrid] Authorizing with key:", apiKey ? "***" : "empty"); + this.instance = axios.create({ + baseURL: this.baseURL, + params: { + agent: "hydra", + apikey: apiKey + } + }); + } + + static async getUser() { + try { + const response = await this.instance.get<{ + status: string; + data?: { user: AllDebridUser }; + error?: { + code: string; + message: string; + }; + }>("/user"); + + logger.info("[AllDebrid] API Response:", response.data); + + if (response.data.status === "error") { + const error = response.data.error; + logger.error("[AllDebrid] API Error:", error); + if (error?.code === "AUTH_MISSING_APIKEY") { + return { error_code: "alldebrid_missing_key" }; + } + if (error?.code === "AUTH_BAD_APIKEY") { + return { error_code: "alldebrid_invalid_key" }; + } + if (error?.code === "AUTH_BLOCKED") { + return { error_code: "alldebrid_blocked" }; + } + if (error?.code === "AUTH_USER_BANNED") { + return { error_code: "alldebrid_banned" }; + } + return { error_code: "alldebrid_unknown_error" }; + } + + if (!response.data.data?.user) { + logger.error("[AllDebrid] No user data in response"); + return { error_code: "alldebrid_invalid_response" }; + } + + logger.info("[AllDebrid] Successfully got user:", response.data.data.user.username); + return { user: response.data.data.user }; + } catch (error: any) { + logger.error("[AllDebrid] Request Error:", error); + if (error.response?.data?.error) { + return { error_code: "alldebrid_invalid_key" }; + } + return { error_code: "alldebrid_network_error" }; + } + } +} diff --git a/src/preload/index.ts b/src/preload/index.ts index ef61cbb9..a6cdcf05 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -92,6 +92,8 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("autoLaunch", autoLaunchProps), authenticateRealDebrid: (apiToken: string) => ipcRenderer.invoke("authenticateRealDebrid", apiToken), + authenticateAllDebrid: (apiKey: string) => + ipcRenderer.invoke("authenticateAllDebrid", apiKey), authenticateTorBox: (apiToken: string) => ipcRenderer.invoke("authenticateTorBox", apiToken), diff --git a/src/renderer/src/pages/settings/settings-all-debrid.scss b/src/renderer/src/pages/settings/settings-all-debrid.scss new file mode 100644 index 00000000..5efe1e66 --- /dev/null +++ b/src/renderer/src/pages/settings/settings-all-debrid.scss @@ -0,0 +1,12 @@ +.settings-all-debrid { + &__form { + display: flex; + flex-direction: column; + gap: 1rem; + } + + &__description { + margin: 0; + color: var(--text-secondary); + } +} \ No newline at end of file diff --git a/src/renderer/src/pages/settings/settings-all-debrid.tsx b/src/renderer/src/pages/settings/settings-all-debrid.tsx new file mode 100644 index 00000000..92b63404 --- /dev/null +++ b/src/renderer/src/pages/settings/settings-all-debrid.tsx @@ -0,0 +1,129 @@ +import { useContext, useEffect, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; + +import { Button, CheckboxField, Link, TextField } from "@renderer/components"; +import "./settings-all-debrid.scss"; + +import { useAppSelector, useToast } from "@renderer/hooks"; + +import { settingsContext } from "@renderer/context"; + +const ALL_DEBRID_API_TOKEN_URL = "https://alldebrid.com/apikeys"; + +export function SettingsAllDebrid() { + const userPreferences = useAppSelector( + (state) => state.userPreferences.value + ); + + const { updateUserPreferences } = useContext(settingsContext); + + const [isLoading, setIsLoading] = useState(false); + const [form, setForm] = useState({ + useAllDebrid: false, + allDebridApiKey: null as string | null, + }); + + const { showSuccessToast, showErrorToast } = useToast(); + + const { t } = useTranslation("settings"); + + useEffect(() => { + if (userPreferences) { + setForm({ + useAllDebrid: Boolean(userPreferences.allDebridApiKey), + allDebridApiKey: userPreferences.allDebridApiKey ?? null, + }); + } + }, [userPreferences]); + + const handleFormSubmit: React.FormEventHandler = async ( + event + ) => { + setIsLoading(true); + event.preventDefault(); + + try { + if (form.useAllDebrid) { + if (!form.allDebridApiKey) { + showErrorToast(t("alldebrid_missing_key")); + return; + } + + const result = await window.electron.authenticateAllDebrid( + form.allDebridApiKey + ); + + if ('error_code' in result) { + showErrorToast(t(result.error_code)); + return; + } + + if (!result.isPremium) { + showErrorToast( + t("all_debrid_free_account_error", { username: result.username }) + ); + return; + } + + showSuccessToast( + t("all_debrid_account_linked"), + t("debrid_linked_message", { username: result.username }) + ); + } else { + showSuccessToast(t("changes_saved")); + } + + updateUserPreferences({ + allDebridApiKey: form.useAllDebrid ? form.allDebridApiKey : null, + }); + } catch (err: any) { + showErrorToast(t("alldebrid_unknown_error")); + } finally { + setIsLoading(false); + } + }; + + const isButtonDisabled = + (form.useAllDebrid && !form.allDebridApiKey) || isLoading; + + return ( +
+

+ {t("all_debrid_description")} +

+ + + setForm((prev) => ({ + ...prev, + useAllDebrid: !form.useAllDebrid, + })) + } + /> + + {form.useAllDebrid && ( + + setForm({ ...form, allDebridApiKey: event.target.value }) + } + rightContent={ + + } + placeholder="API Key" + hint={ + + + + } + /> + )} + + ); +} \ No newline at end of file diff --git a/src/renderer/src/pages/settings/settings.tsx b/src/renderer/src/pages/settings/settings.tsx index 4c94343c..5ff85d0c 100644 --- a/src/renderer/src/pages/settings/settings.tsx +++ b/src/renderer/src/pages/settings/settings.tsx @@ -1,6 +1,7 @@ import { Button } from "@renderer/components"; import { useTranslation } from "react-i18next"; import { SettingsRealDebrid } from "./settings-real-debrid"; +import { SettingsAllDebrid } from "./settings-all-debrid"; import { SettingsGeneral } from "./settings-general"; import { SettingsBehavior } from "./settings-behavior"; import torBoxLogo from "@renderer/assets/icons/torbox.webp"; @@ -35,6 +36,7 @@ export default function Settings() { contentTitle: "TorBox", }, { tabLabel: "Real-Debrid", contentTitle: "Real-Debrid" }, + { tabLabel: "All-Debrid", contentTitle: "All-Debrid" }, ]; if (userDetails) @@ -70,6 +72,10 @@ export default function Settings() { return ; } + if (currentCategoryIndex === 5) { + return ; + } + return ; }; diff --git a/src/types/download.types.ts b/src/types/download.types.ts index 8b7f2091..7f3ef442 100644 --- a/src/types/download.types.ts +++ b/src/types/download.types.ts @@ -174,3 +174,11 @@ export interface SeedingStatus { status: DownloadStatus; uploadSpeed: number; } + +/* All-Debrid */ +export interface AllDebridUser { + username: string; + email: string; + isPremium: boolean; + premiumUntil: string; +} diff --git a/src/types/level.types.ts b/src/types/level.types.ts index 2956165a..9abac9a3 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -70,6 +70,7 @@ export interface UserPreferences { downloadsPath?: string | null; language?: string; realDebridApiToken?: string | null; + allDebridApiKey?: string | null; torBoxApiToken?: string | null; preferQuitInsteadOfHiding?: boolean; runAtStartup?: boolean;