refactor: improve error handling and download path preparation in JsHttpDownloader

This commit is contained in:
Moyasee
2026-01-06 17:56:46 +02:00
parent 569700e85c
commit 8f477072ba
2 changed files with 113 additions and 68 deletions

View File

@@ -450,9 +450,9 @@ export class DownloadManager {
} | null> {
switch (download.downloader) {
case Downloader.Gofile: {
const id = download.uri.split("/").pop();
const id = download.uri.split("/").pop()!;
const token = await GofileApi.authorize();
const downloadLink = await GofileApi.getDownloadLink(id!);
const downloadLink = await GofileApi.getDownloadLink(id);
await GofileApi.checkDownloadUrl(downloadLink);
return {
@@ -462,8 +462,8 @@ export class DownloadManager {
};
}
case Downloader.PixelDrain: {
const id = download.uri.split("/").pop();
const downloadUrl = await PixelDrainApi.getDownloadUrl(id!);
const id = download.uri.split("/").pop()!;
const downloadUrl = await PixelDrainApi.getDownloadUrl(id);
return {
url: downloadUrl,
@@ -576,9 +576,9 @@ export class DownloadManager {
switch (download.downloader) {
case Downloader.Gofile: {
const id = download.uri.split("/").pop();
const id = download.uri.split("/").pop()!;
const token = await GofileApi.authorize();
const downloadLink = await GofileApi.getDownloadLink(id!);
const downloadLink = await GofileApi.getDownloadLink(id);
await GofileApi.checkDownloadUrl(downloadLink);
return {
@@ -592,8 +592,8 @@ export class DownloadManager {
};
}
case Downloader.PixelDrain: {
const id = download.uri.split("/").pop();
const downloadUrl = await PixelDrainApi.getDownloadUrl(id!);
const id = download.uri.split("/").pop()!;
const downloadUrl = await PixelDrainApi.getDownloadUrl(id);
return {
action: "start",

View File

@@ -50,7 +50,28 @@ export class JsHttpDownloader {
this.isDownloading = true;
const { url, savePath, filename, headers = {} } = options;
const { filePath, startByte } = this.prepareDownloadPath(
savePath,
filename,
url
);
const requestHeaders = this.buildRequestHeaders(headers, startByte);
try {
await this.executeDownload(url, requestHeaders, filePath, startByte);
} catch (err) {
this.handleDownloadError(err as Error);
} finally {
this.isDownloading = false;
this.cleanup();
}
}
private prepareDownloadPath(
savePath: string,
filename: string | undefined,
url: string
): { filePath: string; startByte: number } {
const resolvedFilename =
filename || this.extractFilename(url) || "download";
this.folderName = resolvedFilename;
@@ -68,90 +89,114 @@ export class JsHttpDownloader {
logger.log(`[JsHttpDownloader] Resuming download from byte ${startByte}`);
}
// Reset speed tracking to avoid incorrect speed calculation after resume
this.lastSpeedUpdate = Date.now();
this.bytesAtLastSpeedUpdate = this.bytesDownloaded;
this.downloadSpeed = 0;
this.resetSpeedTracking();
return { filePath, startByte };
}
private buildRequestHeaders(
headers: Record<string, string>,
startByte: number
): Record<string, string> {
const requestHeaders: Record<string, string> = { ...headers };
if (startByte > 0) {
requestHeaders["Range"] = `bytes=${startByte}-`;
}
return requestHeaders;
}
try {
const response = await fetch(url, {
headers: requestHeaders,
signal: this.abortController.signal,
});
private resetSpeedTracking(): void {
this.lastSpeedUpdate = Date.now();
this.bytesAtLastSpeedUpdate = this.bytesDownloaded;
this.downloadSpeed = 0;
}
if (!response.ok && response.status !== 206) {
throw new Error(`HTTP error! status: ${response.status}`);
private parseFileSize(response: Response, startByte: number): void {
const contentRange = response.headers.get("content-range");
if (contentRange) {
const match = /bytes \d+-\d+\/(\d+)/.exec(contentRange);
if (match) {
this.fileSize = Number.parseInt(match[1], 10);
}
return;
}
const contentLength = response.headers.get("content-length");
const contentRange = response.headers.get("content-range");
const contentLength = response.headers.get("content-length");
if (contentLength) {
this.fileSize = startByte + Number.parseInt(contentLength, 10);
}
}
if (contentRange) {
const match = contentRange.match(/bytes \d+-\d+\/(\d+)/);
if (match) {
this.fileSize = parseInt(match[1], 10);
}
} else if (contentLength) {
this.fileSize = startByte + parseInt(contentLength, 10);
}
private async executeDownload(
url: string,
requestHeaders: Record<string, string>,
filePath: string,
startByte: number
): Promise<void> {
const response = await fetch(url, {
headers: requestHeaders,
signal: this.abortController!.signal,
});
if (!response.body) {
throw new Error("Response body is null");
}
if (!response.ok && response.status !== 206) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const flags = startByte > 0 ? "a" : "w";
this.writeStream = fs.createWriteStream(filePath, { flags });
this.parseFileSize(response, startByte);
const reader = response.body.getReader();
const onChunk = (length: number) => {
this.bytesDownloaded += length;
this.updateSpeed();
};
if (!response.body) {
throw new Error("Response body is null");
}
const readableStream = new Readable({
async read() {
try {
const { done, value } = await reader.read();
const flags = startByte > 0 ? "a" : "w";
this.writeStream = fs.createWriteStream(filePath, { flags });
const readableStream = this.createReadableStream(response.body.getReader());
await pipeline(readableStream, this.writeStream);
this.status = "complete";
this.downloadSpeed = 0;
logger.log("[JsHttpDownloader] Download complete");
}
private createReadableStream(
reader: ReadableStreamDefaultReader<Uint8Array>
): Readable {
const onChunk = (length: number) => {
this.bytesDownloaded += length;
this.updateSpeed();
};
return new Readable({
read() {
reader
.read()
.then(({ done, value }) => {
if (done) {
this.push(null);
return;
}
onChunk(value.length);
this.push(Buffer.from(value));
} catch (err) {
if ((err as Error).name === "AbortError") {
})
.catch((err: Error) => {
if (err.name === "AbortError") {
this.push(null);
} else {
this.destroy(err as Error);
this.destroy(err);
}
}
},
});
});
},
});
}
await pipeline(readableStream, this.writeStream);
this.status = "complete";
this.downloadSpeed = 0;
logger.log("[JsHttpDownloader] Download complete");
} catch (err) {
if ((err as Error).name === "AbortError") {
logger.log("[JsHttpDownloader] Download aborted");
this.status = "paused";
} else {
logger.error("[JsHttpDownloader] Download error:", err);
this.status = "error";
throw err;
}
} finally {
this.isDownloading = false;
this.cleanup();
private handleDownloadError(err: Error): void {
if (err.name === "AbortError") {
logger.log("[JsHttpDownloader] Download aborted");
this.status = "paused";
} else {
logger.error("[JsHttpDownloader] Download error:", err);
this.status = "error";
throw err;
}
}
@@ -232,7 +277,7 @@ export class JsHttpDownloader {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const pathParts = pathname.split("/");
const filename = pathParts[pathParts.length - 1];
const filename = pathParts.at(-1);
if (filename?.includes(".") && filename.length > 0) {
return decodeURIComponent(filename);