mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-15 16:33:02 -03:00
refactor: enhance filename extraction and handling in download services
This commit is contained in:
@@ -454,34 +454,52 @@ export class DownloadManager {
|
||||
const token = await GofileApi.authorize();
|
||||
const downloadLink = await GofileApi.getDownloadLink(id!);
|
||||
await GofileApi.checkDownloadUrl(downloadLink);
|
||||
const filename =
|
||||
this.extractFilename(download.uri, downloadLink) ||
|
||||
this.extractFilename(downloadLink);
|
||||
|
||||
return {
|
||||
url: downloadLink,
|
||||
savePath: download.downloadPath,
|
||||
filename: filename ? this.sanitizeFilename(filename) : undefined,
|
||||
headers: { Cookie: `accountToken=${token}` },
|
||||
};
|
||||
}
|
||||
case Downloader.PixelDrain: {
|
||||
const id = download.uri.split("/").pop();
|
||||
const downloadUrl = await PixelDrainApi.getDownloadUrl(id!);
|
||||
const filename =
|
||||
this.extractFilename(download.uri, downloadUrl) ||
|
||||
this.extractFilename(downloadUrl);
|
||||
|
||||
return {
|
||||
url: downloadUrl,
|
||||
savePath: download.downloadPath,
|
||||
filename: filename ? this.sanitizeFilename(filename) : undefined,
|
||||
};
|
||||
}
|
||||
case Downloader.Qiwi: {
|
||||
const downloadUrl = await QiwiApi.getDownloadUrl(download.uri);
|
||||
const filename =
|
||||
this.extractFilename(download.uri, downloadUrl) ||
|
||||
this.extractFilename(downloadUrl);
|
||||
|
||||
return {
|
||||
url: downloadUrl,
|
||||
savePath: download.downloadPath,
|
||||
filename: filename ? this.sanitizeFilename(filename) : undefined,
|
||||
};
|
||||
}
|
||||
case Downloader.Datanodes: {
|
||||
const downloadUrl = await DatanodesApi.getDownloadUrl(download.uri);
|
||||
const filename =
|
||||
this.extractFilename(download.uri, downloadUrl) ||
|
||||
this.extractFilename(downloadUrl);
|
||||
|
||||
return {
|
||||
url: downloadUrl,
|
||||
savePath: download.downloadPath,
|
||||
filename: filename ? this.sanitizeFilename(filename) : undefined,
|
||||
};
|
||||
}
|
||||
case Downloader.Buzzheavier: {
|
||||
@@ -516,18 +534,27 @@ export class DownloadManager {
|
||||
}
|
||||
case Downloader.Mediafire: {
|
||||
const downloadUrl = await MediafireApi.getDownloadUrl(download.uri);
|
||||
const filename =
|
||||
this.extractFilename(download.uri, downloadUrl) ||
|
||||
this.extractFilename(downloadUrl);
|
||||
|
||||
return {
|
||||
url: downloadUrl,
|
||||
savePath: download.downloadPath,
|
||||
filename: filename ? this.sanitizeFilename(filename) : undefined,
|
||||
};
|
||||
}
|
||||
case Downloader.RealDebrid: {
|
||||
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
|
||||
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnRealDebrid);
|
||||
const filename =
|
||||
this.extractFilename(download.uri, downloadUrl) ||
|
||||
this.extractFilename(downloadUrl);
|
||||
|
||||
return {
|
||||
url: downloadUrl,
|
||||
savePath: download.downloadPath,
|
||||
filename: filename ? this.sanitizeFilename(filename) : undefined,
|
||||
};
|
||||
}
|
||||
case Downloader.TorBox: {
|
||||
@@ -545,10 +572,14 @@ export class DownloadManager {
|
||||
download.uri
|
||||
);
|
||||
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnHydra);
|
||||
const filename =
|
||||
this.extractFilename(download.uri, downloadUrl) ||
|
||||
this.extractFilename(downloadUrl);
|
||||
|
||||
return {
|
||||
url: downloadUrl,
|
||||
savePath: download.downloadPath,
|
||||
filename: filename ? this.sanitizeFilename(filename) : undefined,
|
||||
};
|
||||
}
|
||||
case Downloader.VikingFile: {
|
||||
|
||||
@@ -50,7 +50,7 @@ export class JsHttpDownloader {
|
||||
this.isDownloading = true;
|
||||
|
||||
const { url, savePath, filename, headers = {} } = options;
|
||||
const { filePath, startByte } = this.prepareDownloadPath(
|
||||
const { filePath, startByte, usedFallback } = this.prepareDownloadPath(
|
||||
savePath,
|
||||
filename,
|
||||
url
|
||||
@@ -58,7 +58,14 @@ export class JsHttpDownloader {
|
||||
const requestHeaders = this.buildRequestHeaders(headers, startByte);
|
||||
|
||||
try {
|
||||
await this.executeDownload(url, requestHeaders, filePath, startByte);
|
||||
await this.executeDownload(
|
||||
url,
|
||||
requestHeaders,
|
||||
filePath,
|
||||
startByte,
|
||||
savePath,
|
||||
usedFallback
|
||||
);
|
||||
} catch (err) {
|
||||
this.handleDownloadError(err as Error);
|
||||
} finally {
|
||||
@@ -71,9 +78,10 @@ export class JsHttpDownloader {
|
||||
savePath: string,
|
||||
filename: string | undefined,
|
||||
url: string
|
||||
): { filePath: string; startByte: number } {
|
||||
const resolvedFilename =
|
||||
filename || this.extractFilename(url) || "download";
|
||||
): { filePath: string; startByte: number; usedFallback: boolean } {
|
||||
const extractedFilename = filename || this.extractFilename(url);
|
||||
const usedFallback = !extractedFilename;
|
||||
const resolvedFilename = extractedFilename || "download";
|
||||
this.folderName = resolvedFilename;
|
||||
const filePath = path.join(savePath, resolvedFilename);
|
||||
|
||||
@@ -90,7 +98,7 @@ export class JsHttpDownloader {
|
||||
}
|
||||
|
||||
this.resetSpeedTracking();
|
||||
return { filePath, startByte };
|
||||
return { filePath, startByte, usedFallback };
|
||||
}
|
||||
|
||||
private buildRequestHeaders(
|
||||
@@ -130,7 +138,9 @@ export class JsHttpDownloader {
|
||||
url: string,
|
||||
requestHeaders: Record<string, string>,
|
||||
filePath: string,
|
||||
startByte: number
|
||||
startByte: number,
|
||||
savePath: string,
|
||||
usedFallback: boolean
|
||||
): Promise<void> {
|
||||
const response = await fetch(url, {
|
||||
headers: requestHeaders,
|
||||
@@ -143,12 +153,25 @@ export class JsHttpDownloader {
|
||||
|
||||
this.parseFileSize(response, startByte);
|
||||
|
||||
// If we used "download" fallback, try to get filename from Content-Disposition
|
||||
let actualFilePath = filePath;
|
||||
if (usedFallback && startByte === 0) {
|
||||
const headerFilename = this.parseContentDisposition(response);
|
||||
if (headerFilename) {
|
||||
actualFilePath = path.join(savePath, headerFilename);
|
||||
this.folderName = headerFilename;
|
||||
logger.log(
|
||||
`[JsHttpDownloader] Using filename from Content-Disposition: ${headerFilename}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error("Response body is null");
|
||||
}
|
||||
|
||||
const flags = startByte > 0 ? "a" : "w";
|
||||
this.writeStream = fs.createWriteStream(filePath, { flags });
|
||||
this.writeStream = fs.createWriteStream(actualFilePath, { flags });
|
||||
|
||||
const readableStream = this.createReadableStream(response.body.getReader());
|
||||
await pipeline(readableStream, this.writeStream);
|
||||
@@ -158,6 +181,25 @@ export class JsHttpDownloader {
|
||||
logger.log("[JsHttpDownloader] Download complete");
|
||||
}
|
||||
|
||||
private parseContentDisposition(response: Response): string | undefined {
|
||||
const header = response.headers.get("content-disposition");
|
||||
if (!header) return undefined;
|
||||
|
||||
// Try to extract filename from Content-Disposition header
|
||||
// Formats: attachment; filename="file.zip" or attachment; filename=file.zip
|
||||
const filenameMatch = /filename\*?=['"]?(?:UTF-8'')?([^"';\n]+)['"]?/i.exec(
|
||||
header
|
||||
);
|
||||
if (filenameMatch?.[1]) {
|
||||
try {
|
||||
return decodeURIComponent(filenameMatch[1].trim());
|
||||
} catch {
|
||||
return filenameMatch[1].trim();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private createReadableStream(
|
||||
reader: ReadableStreamDefaultReader<Uint8Array>
|
||||
): Readable {
|
||||
|
||||
Reference in New Issue
Block a user