Add Somewhat working logic

This commit is contained in:
mircea32000 2025-02-08 18:02:06 +02:00
parent 2a4221e787
commit 5e9aa2b0ea
13 changed files with 461 additions and 54 deletions

View file

@ -1,48 +1,162 @@
import aria2p
from typing import Union, List
import logging
import os
from pathlib import Path
from aria2p import API, Client, Download
import requests
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class HttpDownloader:
def __init__(self):
self.download = None
self.aria2 = aria2p.API(
aria2p.Client(
host="http://localhost",
port=6800,
secret=""
self.downloads = [] # vom păstra toate download-urile active
self.aria2 = API(Client(host="http://localhost", port=6800))
self.download = None # pentru compatibilitate cu codul vechi
def unlock_alldebrid_link(self, link: str) -> str:
"""Deblochează un link AllDebrid și returnează link-ul real de descărcare."""
api_key = os.getenv('ALLDEBRID_API_KEY')
if not api_key:
logger.error("AllDebrid API key nu a fost găsită în variabilele de mediu")
return link
try:
response = requests.post(
"https://api.alldebrid.com/v4/link/unlock",
params={
"agent": "hydra",
"apikey": api_key,
"link": link
}
)
)
data = response.json()
def start_download(self, url: str, save_path: str, header: str, out: str = None):
if self.download:
self.aria2.resume([self.download])
if data.get("status") == "success":
return data["data"]["link"]
else:
logger.error(f"Eroare la deblocarea link-ului AllDebrid: {data.get('error', {}).get('message', 'Unknown error')}")
return link
except Exception as e:
logger.error(f"Eroare la apelul API AllDebrid: {str(e)}")
return link
def start_download(self, url: Union[str, List[str]], save_path: str, header: str = None, out: str = None):
logger.info(f"Starting download with URL: {url}, save_path: {save_path}, header: {header}, out: {out}")
# Pentru AllDebrid care returnează un link per fișier
if isinstance(url, list):
logger.info(f"Multiple URLs detected: {len(url)} files to download")
self.downloads = []
# Deblocăm toate link-urile AllDebrid
unlocked_urls = []
for single_url in url:
logger.info(f"Unlocking AllDebrid URL: {single_url}")
unlocked_url = self.unlock_alldebrid_link(single_url)
if unlocked_url:
unlocked_urls.append(unlocked_url)
logger.info(f"URL deblocat cu succes: {unlocked_url}")
# Descărcăm folosind link-urile deblocate
for unlocked_url in unlocked_urls:
logger.info(f"Adding download for unlocked URL: {unlocked_url}")
options = {
"dir": save_path
}
if header:
if isinstance(header, list):
options["header"] = header
else:
options["header"] = [header]
try:
download = self.aria2.add_uris([unlocked_url], options=options)
logger.info(f"Download added successfully: {download.gid}")
self.downloads.append(download)
except Exception as e:
logger.error(f"Error adding download for URL {unlocked_url}: {str(e)}")
if self.downloads:
self.download = self.downloads[0] # păstrăm primul pentru referință
else:
logger.error("No downloads were successfully added!")
# Pentru RealDebrid/alte servicii care returnează un singur link pentru tot
else:
downloads = self.aria2.add(url, options={"header": header, "dir": save_path, "out": out})
logger.info(f"Single URL download: {url}")
options = {
"dir": save_path
}
if header:
if isinstance(header, list):
options["header"] = header
else:
options["header"] = [header]
if out:
options["out"] = out
self.download = downloads[0]
try:
download = self.aria2.add_uris([url], options=options)
self.download = download
self.downloads = [self.download]
logger.info(f"Single download added successfully: {self.download.gid}")
except Exception as e:
logger.error(f"Error adding single download: {str(e)}")
def pause_download(self):
if self.download:
self.aria2.pause([self.download])
try:
for download in self.downloads:
download.pause()
except Exception as e:
logger.error(f"Error pausing downloads: {str(e)}")
def cancel_download(self):
if self.download:
self.aria2.remove([self.download])
self.download = None
try:
for download in self.downloads:
download.remove()
except Exception as e:
logger.error(f"Error canceling downloads: {str(e)}")
def get_download_status(self):
if self.download == None:
try:
if not self.downloads:
return None
total_size = 0
downloaded = 0
download_speed = 0
active_downloads = []
for download in self.downloads:
try:
download.update()
if download.is_active:
active_downloads.append(download)
total_size += download.total_length
downloaded += download.completed_length
download_speed += download.download_speed
except Exception as e:
logger.error(f"Error updating download status for {download.gid}: {str(e)}")
if not active_downloads:
return None
# Folosim primul download pentru numele folderului
folder_path = os.path.dirname(active_downloads[0].files[0].path)
folder_name = os.path.basename(folder_path)
return {
"progress": downloaded / total_size if total_size > 0 else 0,
"numPeers": 0, # nu este relevant pentru HTTP
"numSeeds": 0, # nu este relevant pentru HTTP
"downloadSpeed": download_speed,
"bytesDownloaded": downloaded,
"fileSize": total_size,
"folderName": folder_name,
"status": "downloading"
}
except Exception as e:
logger.error(f"Error getting download status: {str(e)}")
return None
download = self.aria2.get_download(self.download.gid)
response = {
'folderName': download.name,
'fileSize': download.total_length,
'progress': download.completed_length / download.total_length if download.total_length else 0,
'downloadSpeed': download.download_speed,
'numPeers': 0,
'numSeeds': 0,
'status': download.status,
'bytesDownloaded': download.completed_length,
}
return response

View file

@ -23,19 +23,27 @@ torrent_session = lt.session({'listen_interfaces': '0.0.0.0:{port}'.format(port=
if start_download_payload:
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
downloading_game_id = initial_download['game_id']
url = initial_download['url']
if initial_download['url'].startswith('magnet'):
# Verificăm dacă avem un URL de tip magnet (fie direct, fie primul dintr-o listă)
is_magnet = False
if isinstance(url, str):
is_magnet = url.startswith('magnet')
elif isinstance(url, list) and url:
is_magnet = False # Pentru AllDebrid, chiar dacă vine dintr-un magnet, primim HTTP links
if is_magnet:
torrent_downloader = TorrentDownloader(torrent_session)
downloads[initial_download['game_id']] = torrent_downloader
try:
torrent_downloader.start_download(initial_download['url'], initial_download['save_path'])
torrent_downloader.start_download(url, initial_download['save_path'])
except Exception as e:
print("Error starting torrent download", e)
else:
http_downloader = HttpDownloader()
downloads[initial_download['game_id']] = http_downloader
try:
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
http_downloader.start_download(url, initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
except Exception as e:
print("Error starting http download", e)
@ -135,10 +143,18 @@ def action():
if action == 'start':
url = data.get('url')
print(f"Starting download with URL: {url}")
existing_downloader = downloads.get(game_id)
if url.startswith('magnet'):
# Verificăm dacă avem un URL de tip magnet (fie direct, fie primul dintr-o listă)
is_magnet = False
if isinstance(url, str):
is_magnet = url.startswith('magnet')
elif isinstance(url, list) and url:
is_magnet = False # Pentru AllDebrid, chiar dacă vine dintr-un magnet, primim HTTP links
if is_magnet:
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
existing_downloader.start_download(url, data['save_path'])
else:
@ -172,7 +188,6 @@ def action():
downloader = downloads.get(game_id)
if downloader:
downloader.cancel_download()
else:
return jsonify({"error": "Invalid action"}), 400

View file

@ -2,6 +2,31 @@ import axios, { AxiosInstance } from "axios";
import type { AllDebridUser } from "@types";
import { logger } from "@main/services";
interface AllDebridMagnetStatus {
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;
}>;
}
interface AllDebridError {
code: string;
message: string;
}
export class AllDebridClient {
private static instance: AxiosInstance;
private static readonly baseURL = "https://api.alldebrid.com/v4";
@ -9,11 +34,11 @@ export class AllDebridClient {
static authorize(apiKey: string) {
logger.info("[AllDebrid] Authorizing with key:", apiKey ? "***" : "empty");
this.instance = axios.create({
baseURL: this.baseURL,
params: {
agent: "hydra",
apikey: apiKey
}
baseURL: this.baseURL,
params: {
agent: "hydra",
apikey: apiKey
}
});
}
@ -22,10 +47,7 @@ export class AllDebridClient {
const response = await this.instance.get<{
status: string;
data?: { user: AllDebridUser };
error?: {
code: string;
message: string;
};
error?: AllDebridError;
}>("/user");
logger.info("[AllDebrid] API Response:", response.data);
@ -63,4 +85,175 @@ export class AllDebridClient {
return { error_code: "alldebrid_network_error" };
}
}
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;
}
}
private static async checkMagnetStatus(magnetId: number): Promise<AllDebridMagnetStatus> {
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<string[]> {
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 unlockedLink;
} 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 [downloadUrl];
}
} catch (error: any) {
logger.error("[AllDebrid] Get Download URLs Error:", error);
throw error;
}
}
}

View file

@ -16,6 +16,8 @@ import { logger } from "../logger";
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";
export class DownloadManager {
private static downloadingGameId: string | null = null;
@ -333,6 +335,18 @@ 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);
return {
action: "start",
game_id: downloadId,
url: downloadUrls,
save_path: download.downloadPath,
};
}
case Downloader.TorBox: {
const { name, url } = await TorBoxClient.getDownloadInfo(download.uri);

View file

@ -8,6 +8,8 @@ import crypto from "node:crypto";
import { pythonRpcLogger } from "./logger";
import { Readable } from "node:stream";
import { app, dialog } from "electron";
import { db, levelKeys } from "@main/level";
import type { UserPreferences } from "@types";
interface GamePayload {
game_id: string;
@ -42,7 +44,7 @@ export class PythonRPC {
readable.on("data", pythonRpcLogger.log);
}
public static spawn(
public static async spawn(
initialDownload?: GamePayload,
initialSeeding?: GamePayload[]
) {
@ -54,6 +56,15 @@ export class PythonRPC {
initialSeeding ? JSON.stringify(initialSeeding) : "",
];
const userPreferences = await db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
valueEncoding: "json",
});
const env = {
...process.env,
ALLDEBRID_API_KEY: userPreferences?.allDebridApiKey || ""
};
if (app.isPackaged) {
const binaryName = binaryNameByPlatform[process.platform]!;
const binaryPath = path.join(
@ -74,6 +85,7 @@ export class PythonRPC {
const childProcess = cp.spawn(binaryPath, commonArgs, {
windowsHide: true,
stdio: ["inherit", "inherit"],
env
});
this.logStderr(childProcess.stderr);
@ -90,6 +102,7 @@ export class PythonRPC {
const childProcess = cp.spawn("python3", [scriptPath, ...commonArgs], {
stdio: ["inherit", "inherit"],
env
});
this.logStderr(childProcess.stderr);

View file

@ -11,6 +11,7 @@ export const DOWNLOADER_NAME = {
[Downloader.Datanodes]: "Datanodes",
[Downloader.Mediafire]: "Mediafire",
[Downloader.TorBox]: "TorBox",
[Downloader.AllDebrid]: "All-Debrid",
};
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;

View file

@ -0,0 +1,13 @@
import { Downloader } from "@shared";
export const DOWNLOADER_NAME: Record<Downloader, string> = {
[Downloader.Gofile]: "Gofile",
[Downloader.PixelDrain]: "PixelDrain",
[Downloader.Qiwi]: "Qiwi",
[Downloader.Datanodes]: "Datanodes",
[Downloader.Mediafire]: "Mediafire",
[Downloader.Torrent]: "Torrent",
[Downloader.RealDebrid]: "Real-Debrid",
[Downloader.AllDebrid]: "All-Debrid",
[Downloader.TorBox]: "TorBox",
};

View file

@ -240,7 +240,9 @@ export function DownloadGroup({
(download?.downloader === Downloader.RealDebrid &&
!userPreferences?.realDebridApiToken) ||
(download?.downloader === Downloader.TorBox &&
!userPreferences?.torBoxApiToken);
!userPreferences?.torBoxApiToken) ||
(download?.downloader === Downloader.AllDebrid &&
!userPreferences?.allDebridApiKey);
return [
{

View file

@ -27,6 +27,11 @@
&__downloader-option {
position: relative;
padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 6);
text-align: left;
display: flex;
align-items: center;
min-width: 150px;
&:only-child {
grid-column: 1 / -1;
@ -36,6 +41,8 @@
&__downloader-icon {
position: absolute;
left: calc(globals.$spacing-unit * 2);
top: 50%;
transform: translateY(-50%);
}
&__path-error {

View file

@ -87,6 +87,8 @@ export function DownloadSettingsModal({
return userPreferences?.realDebridApiToken;
if (downloader === Downloader.TorBox)
return userPreferences?.torBoxApiToken;
if (downloader === Downloader.AllDebrid)
return userPreferences?.allDebridApiKey;
return true;
});
@ -100,6 +102,8 @@ export function DownloadSettingsModal({
userPreferences?.downloadsPath,
downloaders,
userPreferences?.realDebridApiToken,
userPreferences?.torBoxApiToken,
userPreferences?.allDebridApiKey,
]);
const handleChooseDownloadsPath = async () => {
@ -163,8 +167,10 @@ export function DownloadSettingsModal({
selectedDownloader === downloader ? "primary" : "outline"
}
disabled={
downloader === Downloader.RealDebrid &&
!userPreferences?.realDebridApiToken
(downloader === Downloader.RealDebrid &&
!userPreferences?.realDebridApiToken) ||
(downloader === Downloader.AllDebrid &&
!userPreferences?.allDebridApiKey)
}
onClick={() => setSelectedDownloader(downloader)}
>

View file

@ -7,6 +7,7 @@ export enum Downloader {
Datanodes,
Mediafire,
TorBox,
AllDebrid,
}
export enum DownloadSourceStatus {
@ -54,6 +55,7 @@ export enum AuthPage {
export enum DownloadError {
NotCachedInRealDebrid = "download_error_not_cached_in_real_debrid",
NotCachedInTorbox = "download_error_not_cached_in_torbox",
NotCachedInAllDebrid = "download_error_not_cached_in_alldebrid",
GofileQuotaExceeded = "download_error_gofile_quota_exceeded",
RealDebridAccountNotAuthorized = "download_error_real_debrid_account_not_authorized",
}

View file

@ -95,7 +95,7 @@ export const getDownloadersForUri = (uri: string) => {
return [Downloader.RealDebrid];
if (uri.startsWith("magnet:")) {
return [Downloader.Torrent, Downloader.TorBox, Downloader.RealDebrid];
return [Downloader.Torrent, Downloader.TorBox, Downloader.RealDebrid, Downloader.AllDebrid];
}
return [];

View file

@ -182,3 +182,30 @@ export interface AllDebridUser {
isPremium: boolean;
premiumUntil: string;
}
export enum Downloader {
Gofile = "gofile",
PixelDrain = "pixeldrain",
Qiwi = "qiwi",
Datanodes = "datanodes",
Mediafire = "mediafire",
Torrent = "torrent",
RealDebrid = "realdebrid",
AllDebrid = "alldebrid",
TorBox = "torbox",
}
export enum DownloadError {
NotCachedInRealDebrid = "not_cached_in_realdebrid",
NotCachedInAllDebrid = "not_cached_in_alldebrid",
// ... alte erori existente
}
export interface GamePayload {
action: string;
game_id: string;
url: string | string[]; // Modificăm pentru a accepta și array de URL-uri
save_path: string;
header?: string;
out?: string;
}