From ca2f70aede9fe15b26602ee62b41a4188db8c9ba Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 6 Jan 2026 18:50:36 +0200 Subject: [PATCH] refactor: enhance filename extraction and handling in download services --- .../services/download/download-manager.ts | 31 ++++++++++ .../services/download/js-http-downloader.ts | 58 ++++++++++++++++--- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index 497bc326..715fe290 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -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: { diff --git a/src/main/services/download/js-http-downloader.ts b/src/main/services/download/js-http-downloader.ts index 5f0af779..e5596baf 100644 --- a/src/main/services/download/js-http-downloader.ts +++ b/src/main/services/download/js-http-downloader.ts @@ -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, filePath: string, - startByte: number + startByte: number, + savePath: string, + usedFallback: boolean ): Promise { 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 ): Readable {