-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #755 from hydralauncher/feature/aria2-for-http-dow…
…nloads Feature/aria2 for http downloads
- Loading branch information
Showing
10 changed files
with
288 additions
and
139 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.