Skip to content

Commit

Permalink
Merge pull request #755 from hydralauncher/feature/aria2-for-http-dow…
Browse files Browse the repository at this point in the history
…nloads

Feature/aria2 for http downloads
  • Loading branch information
thegrannychaseroperation authored Jul 3, 2024
2 parents 56c8349 + 26aad17 commit be1d982
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 139 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.vscode
node_modules
hydra-download-manager/
aria2/
fastlist.exe
__pycache__
dist
out
Expand Down
1 change: 1 addition & 0 deletions electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ productName: Hydra
directories:
buildResources: build
extraResources:
- aria2
- hydra-download-manager
- seeds
- from: node_modules/create-desktop-shortcuts/src/windows.vbs
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"postinstall": "electron-builder install-app-deps && node ./postinstall.cjs",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "electron-vite build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
Expand All @@ -41,6 +41,7 @@
"@sentry/electron": "^5.1.0",
"@vanilla-extract/css": "^1.14.2",
"@vanilla-extract/recipes": "^0.5.2",
"aria2": "^4.1.2",
"auto-launch": "^5.0.6",
"axios": "^1.6.8",
"better-sqlite3": "^9.5.0",
Expand Down
50 changes: 50 additions & 0 deletions postinstall.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const { default: axios } = require("axios");
const util = require("node:util");
const fs = require("node:fs");

const exec = util.promisify(require("node:child_process").exec);

const downloadAria2 = async () => {
if (fs.existsSync("aria2")) {
console.log("Aria2 already exists, skipping download...");
return;
}

const file =
process.platform === "win32"
? "aria2-1.37.0-win-64bit-build1.zip"
: "aria2-1.37.0-1-x86_64.pkg.tar.zst";

const downloadUrl =
process.platform === "win32"
? `https://github.com/aria2/aria2/releases/download/release-1.37.0/${file}`
: "https://archlinux.org/packages/extra/x86_64/aria2/download/";

console.log(`Downloading ${file}...`);

const response = await axios.get(downloadUrl, { responseType: "stream" });

const stream = response.data.pipe(fs.createWriteStream(file));

stream.on("finish", async () => {
console.log(`Downloaded ${file}, extracting...`);

if (process.platform === "win32") {
await exec(`npx extract-zip ${file}`);
console.log("Extracted. Renaming folder...");

fs.renameSync(file.replace(".zip", ""), "aria2");
} else {
await exec(`tar --zstd -xvf ${file} usr/bin/aria2c`);
console.log("Extracted. Copying binary file...");
fs.mkdirSync("aria2");
fs.copyFileSync("usr/bin/aria2c", "aria2/aria2c");
fs.rmSync("usr", { recursive: true });
}

console.log(`Extracted ${file}, removing compressed downloaded file...`);
fs.rmSync(file);
});
};

downloadAria2();
80 changes: 80 additions & 0 deletions src/main/declaration.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
declare module "aria2" {
export type Aria2Status =
| "active"
| "waiting"
| "paused"
| "error"
| "complete"
| "removed";

export interface StatusResponse {
gid: string;
status: Aria2Status;
totalLength: string;
completedLength: string;
uploadLength: string;
bitfield: string;
downloadSpeed: string;
uploadSpeed: string;
infoHash?: string;
numSeeders?: string;
seeder?: boolean;
pieceLength: string;
numPieces: string;
connections: string;
errorCode?: string;
errorMessage?: string;
followedBy?: string[];
following: string;
belongsTo: string;
dir: string;
files: {
path: string;
length: string;
completedLength: string;
selected: string;
}[];
bittorrent?: {
announceList: string[][];
comment: string;
creationDate: string;
mode: "single" | "multi";
info: {
name: string;
verifiedLength: string;
verifyIntegrityPending: string;
};
};
}

export default class Aria2 {
constructor(options: any);
open: () => Promise<void>;
call(
method: "addUri",
uris: string[],
options: { dir: string }
): Promise<string>;
call(
method: "tellStatus",
gid: string,
keys?: string[]
): Promise<StatusResponse>;
call(method: "pause", gid: string): Promise<string>;
call(method: "forcePause", gid: string): Promise<string>;
call(method: "unpause", gid: string): Promise<string>;
call(method: "remove", gid: string): Promise<string>;
call(method: "forceRemove", gid: string): Promise<string>;
call(method: "pauseAll"): Promise<string>;
call(method: "forcePauseAll"): Promise<string>;
listNotifications: () => [
"onDownloadStart",
"onDownloadPause",
"onDownloadStop",
"onDownloadComplete",
"onDownloadError",
"onBtDownloadComplete",
];
on: (event: string, callback: (params: any) => void) => void;
}
}
20 changes: 20 additions & 0 deletions src/main/services/aria2c.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import path from "node:path";
import { spawn } from "node:child_process";
import { app } from "electron";

export const startAria2 = () => {
const binaryPath = app.isPackaged
? path.join(process.resourcesPath, "aria2", "aria2c")
: path.join(__dirname, "..", "..", "aria2", "aria2c");

return spawn(
binaryPath,
[
"--enable-rpc",
"--rpc-listen-all",
"--file-allocation=none",
"--allow-overwrite=true",
],
{ stdio: "inherit", windowsHide: true }
);
};
4 changes: 2 additions & 2 deletions src/main/services/download/download-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class DownloadManager {

static async pauseDownload() {
if (this.currentDownloader === Downloader.RealDebrid) {
RealDebridDownloader.pauseDownload();
await RealDebridDownloader.pauseDownload();
} else {
await PythonInstance.pauseDownload();
}
Expand All @@ -84,7 +84,7 @@ export class DownloadManager {

static async cancelDownload(gameId: number) {
if (this.currentDownloader === Downloader.RealDebrid) {
RealDebridDownloader.cancelDownload();
RealDebridDownloader.cancelDownload(gameId);
} else {
PythonInstance.cancelDownload(gameId);
}
Expand Down
141 changes: 43 additions & 98 deletions src/main/services/download/http-download.ts
Original file line number Diff line number Diff line change
@@ -1,123 +1,68 @@
import path from "node:path";
import fs from "node:fs";
import crypto from "node:crypto";

import axios, { type AxiosProgressEvent } from "axios";
import { app } from "electron";
import type { ChildProcess } from "node:child_process";
import { logger } from "../logger";
import { sleep } from "@main/helpers";
import { startAria2 } from "../aria2c";
import Aria2 from "aria2";

export class HttpDownload {
private abortController: AbortController;
public lastProgressEvent: AxiosProgressEvent;
private trackerFilePath: string;

private trackerProgressEvent: AxiosProgressEvent | null = null;
private downloadPath: string;
private static connected = false;
private static aria2c: ChildProcess | null = null;

private downloadTrackersPath = path.join(
app.getPath("documents"),
"Hydra",
"Downloads"
);
private static aria2 = new Aria2({});

constructor(
private url: string,
private savePath: string
) {
this.abortController = new AbortController();
private static async connect() {
this.aria2c = startAria2();

const sha256Hasher = crypto.createHash("sha256");
const hash = sha256Hasher.update(url).digest("hex");
let retries = 0;

this.trackerFilePath = path.join(
this.downloadTrackersPath,
`${hash}.hydradownload`
);
while (retries < 4 && !this.connected) {
try {
await this.aria2.open();
logger.log("Connected to aria2");

const filename = path.win32.basename(this.url);
this.downloadPath = path.join(this.savePath, filename);
this.connected = true;
} catch (err) {
await sleep(100);
logger.log("Failed to connect to aria2, retrying...");
retries++;
}
}
}

private updateTrackerFile() {
if (!fs.existsSync(this.downloadTrackersPath)) {
fs.mkdirSync(this.downloadTrackersPath, {
recursive: true,
});
public static getStatus(gid: string) {
if (this.connected) {
return this.aria2.call("tellStatus", gid);
}

fs.writeFileSync(
this.trackerFilePath,
JSON.stringify(this.lastProgressEvent),
{ encoding: "utf-8" }
);
return null;
}

private removeTrackerFile() {
if (fs.existsSync(this.trackerFilePath)) {
fs.rm(this.trackerFilePath, (err) => {
logger.error(err);
});
public static disconnect() {
if (this.aria2c) {
this.aria2c.kill();
this.connected = false;
}
}

public async startDownload() {
// Check if there's already a tracker file and download file
if (
fs.existsSync(this.trackerFilePath) &&
fs.existsSync(this.downloadPath)
) {
this.trackerProgressEvent = JSON.parse(
fs.readFileSync(this.trackerFilePath, { encoding: "utf-8" })
);
}

const response = await axios.get(this.url, {
responseType: "stream",
signal: this.abortController.signal,
headers: {
Range: `bytes=${this.trackerProgressEvent?.loaded ?? 0}-`,
},
onDownloadProgress: (progressEvent) => {
const total =
this.trackerProgressEvent?.total ?? progressEvent.total ?? 0;
const loaded =
(this.trackerProgressEvent?.loaded ?? 0) + progressEvent.loaded;

const progress = loaded / total;

this.lastProgressEvent = {
...progressEvent,
total,
progress,
loaded,
};
this.updateTrackerFile();

if (progressEvent.progress === 1) {
this.removeTrackerFile();
}
},
});
static async cancelDownload(gid: string) {
await this.aria2.call("forceRemove", gid);
}

response.data.pipe(
fs.createWriteStream(this.downloadPath, {
flags: "a",
})
);
static async pauseDownload(gid: string) {
await this.aria2.call("forcePause", gid);
}

public async pauseDownload() {
this.abortController.abort();
static async resumeDownload(gid: string) {
await this.aria2.call("unpause", gid);
}

public cancelDownload() {
this.pauseDownload();
static async startDownload(downloadPath: string, downloadUrl: string) {
if (!this.connected) await this.connect();

const options = {
dir: downloadPath,
};

fs.rm(this.downloadPath, (err) => {
if (err) logger.error(err);
});
fs.rm(this.trackerFilePath, (err) => {
if (err) logger.error(err);
});
return this.aria2.call("addUri", [downloadUrl], options);
}
}
Loading

0 comments on commit be1d982

Please sign in to comment.