From 5b92e0b0e0c76ee428ac4a04f6f17c7dfc203398 Mon Sep 17 00:00:00 2001 From: Theo Gravity Date: Mon, 22 Jul 2024 23:57:53 -0700 Subject: [PATCH] Add support for new Electron DownloadItem APIs --- CHANGELOG.md | 6 +++ package-lock.json | 24 ++++------- package.json | 4 +- src/DownloadInitiator.ts | 13 +++--- src/__mocks__/DownloadData.ts | 3 ++ src/utils.ts | 39 +++++++++--------- test/DownloadInitiator.test.ts | 30 +++++++------- test/utils.test.ts | 74 +++++++++++++++++++++++++--------- 8 files changed, 113 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f26f95..cd65efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 3.1.0 (2024-07-22) + +- If you are using Electron >= `30.3.0`, you will get native reporting on +download percent and bytes per second via the Electron API instead of manual calculations. + * Provided via [this Electron PR](https://github.com/electron/electron/pull/42914) + # 3.0.1 (2024-07-13) Do not emit progress events when `pause()` is called. diff --git a/package-lock.json b/package-lock.json index 081913d..168998c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "electron-dl-manager", - "version": "3.0.0", + "version": "3.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "electron-dl-manager", - "version": "3.0.0", + "version": "3.0.1", "license": "MIT", "dependencies": { "ext-name": "^5.0.0", @@ -16,7 +16,7 @@ "@biomejs/biome": "1.6.3", "@types/jest": "^29.5.12", "add": "^2.0.6", - "electron": "28.2.7", + "electron": "30.3.0", "jest": "^29.7.0", "ts-jest": "^29.1.2", "ts-loader": "^9.5.1", @@ -2710,14 +2710,15 @@ } }, "node_modules/electron": { - "version": "28.2.7", - "resolved": "https://registry.npmjs.org/electron/-/electron-28.2.7.tgz", - "integrity": "sha512-iEBTYNFuZtLpAS+8ql0ATUWBPAC9uMYqwNJtMLqlT3/zOzHj6aYpwoJILwWgIuTAx+/yTYgARS46Nr/RazxTpg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-30.3.0.tgz", + "integrity": "sha512-/rWPcpCL4sYCUm1bY8if1dO8nyFTwXlPUP0dpL3ir5iLK/9NshN6lIJ8xceEY8CEYVLMIYRkxXb44Q9cdrjtOQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^18.11.18", + "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { @@ -2733,15 +2734,6 @@ "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==", "dev": true }, - "node_modules/electron/node_modules/@types/node": { - "version": "18.19.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.26.tgz", - "integrity": "sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", diff --git a/package.json b/package.json index ec47b13..0a52ae5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "electron-dl-manager", - "version": "3.0.1", + "version": "3.1.0", "description": "A library for implementing file downloads in Electron with 'save as' dialog and id support.", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -58,7 +58,7 @@ "@biomejs/biome": "1.6.3", "@types/jest": "^29.5.12", "add": "^2.0.6", - "electron": "28.2.7", + "electron": "30.3.0", "jest": "^29.7.0", "ts-jest": "^29.1.2", "ts-loader": "^9.5.1", diff --git a/src/DownloadInitiator.ts b/src/DownloadInitiator.ts index b23a070..152d7e3 100644 --- a/src/DownloadInitiator.ts +++ b/src/DownloadInitiator.ts @@ -244,17 +244,14 @@ export class DownloadInitiator { protected updateProgress() { const { item } = this.downloadData; - const input = { - downloadedBytes: item.getReceivedBytes(), - totalBytes: item.getTotalBytes(), - startTimeSecs: item.getStartTime(), - }; + const metrics = calculateDownloadMetrics(item); - const metrics = calculateDownloadMetrics(input); + const downloadedBytes = item.getReceivedBytes(); + const totalBytes = item.getTotalBytes(); - if (input.downloadedBytes > input.totalBytes) { + if (downloadedBytes > item.getTotalBytes()) { // Note: This situation will happen when using data: URIs - this.log(`Downloaded bytes (${input.downloadedBytes}) is greater than total bytes (${input.totalBytes})`); + this.log(`Downloaded bytes (${downloadedBytes}) is greater than total bytes (${totalBytes})`); } this.downloadData.downloadRateBytesPerSecond = metrics.downloadRateBytesPerSecond; diff --git a/src/__mocks__/DownloadData.ts b/src/__mocks__/DownloadData.ts index ab7262f..8e31ae1 100644 --- a/src/__mocks__/DownloadData.ts +++ b/src/__mocks__/DownloadData.ts @@ -15,6 +15,9 @@ export function createMockDownloadData() { getSavePath: jest.fn().mockReturnValue("/path/to/save"), getReceivedBytes: jest.fn().mockReturnValue(900), getTotalBytes: jest.fn().mockReturnValue(1000), + getCurrentBytesPerSecond: jest.fn(), + getPercentComplete: jest.fn(), + getStartTime: jest.fn(), cancel: jest.fn(), pause: jest.fn(), resume: jest.fn(), diff --git a/src/utils.ts b/src/utils.ts index 96180a6..5895f8b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -69,43 +69,44 @@ export function determineFilePath({ } /** - * Calculates the download rate and estimated time remaining, using the start time and current time to determine elapsed time. - * - * @param {object} params - An object containing the parameters for the calculation. - * @param {number} params.totalBytes - The total size of the download in bytes. - * @param {number} params.downloadedBytes - The amount of data downloaded so far in bytes. - * @param {number} params.startTimeSecs - The start time of the download in seconds. + * Calculates the download rate and estimated time remaining for a download. * @returns {object} An object containing the download rate in bytes per second and the estimated time remaining in seconds. */ -export function calculateDownloadMetrics({ - totalBytes, - downloadedBytes, - startTimeSecs, -}: { - totalBytes: number; - downloadedBytes: number; - startTimeSecs: number; -}): { +export function calculateDownloadMetrics(item: DownloadItem): { percentCompleted: number; downloadRateBytesPerSecond: number; estimatedTimeRemainingSeconds: number; } { + const downloadedBytes = item.getReceivedBytes(); + const totalBytes = item.getTotalBytes(); + const startTimeSecs = item.getStartTime(); + const currentTimeSecs = Math.floor(new Date().getTime() / 1000); const elapsedTimeSecs = currentTimeSecs - startTimeSecs; - let downloadRateBytesPerSecond = 0; + // Avail in Electron 30.3.0+ + let downloadRateBytesPerSecond = item.getCurrentBytesPerSecond ? item.getCurrentBytesPerSecond() : 0; let estimatedTimeRemainingSeconds = 0; if (elapsedTimeSecs > 0) { - downloadRateBytesPerSecond = downloadedBytes / elapsedTimeSecs; + if (!downloadRateBytesPerSecond) { + downloadRateBytesPerSecond = downloadedBytes / elapsedTimeSecs; + } if (downloadRateBytesPerSecond > 0) { estimatedTimeRemainingSeconds = (totalBytes - downloadedBytes) / downloadRateBytesPerSecond; } } - const percentCompleted = - totalBytes > 0 ? Math.min(Number.parseFloat(((downloadedBytes / totalBytes) * 100).toFixed(2)), 100) : 0; + let percentCompleted = 0; + + // Avail in Electron 30.3.0+ + if (item.getPercentComplete) { + percentCompleted = item.getPercentComplete(); + } else { + percentCompleted = + totalBytes > 0 ? Math.min(Number.parseFloat(((downloadedBytes / totalBytes) * 100).toFixed(2)), 100) : 0; + } return { percentCompleted, diff --git a/test/DownloadInitiator.test.ts b/test/DownloadInitiator.test.ts index f00608f..f9d3a66 100644 --- a/test/DownloadInitiator.test.ts +++ b/test/DownloadInitiator.test.ts @@ -291,25 +291,25 @@ describe("DownloadInitiator", () => { }); it("should call the item updated event if the download was paused and resumed", async () => { - const downloadInitiator = new DownloadInitiator({}); - downloadInitiator.downloadData = mockDownloadData; - downloadInitiator.updateProgress = jest.fn(); + const downloadInitiator = new DownloadInitiator({}); + downloadInitiator.downloadData = mockDownloadData; + downloadInitiator.updateProgress = jest.fn(); - determineFilePath.mockReturnValueOnce("/some/path/test.txt"); + determineFilePath.mockReturnValueOnce("/some/path/test.txt"); - await downloadInitiator.generateOnWillDownload({ - callbacks, - })(mockEvent, mockItem, mockWebContents); + await downloadInitiator.generateOnWillDownload({ + callbacks, + })(mockEvent, mockItem, mockWebContents); - await jest.runAllTimersAsync(); + await jest.runAllTimersAsync(); - mockItem.pause(); - mockEmitter.emit("updated", "", "progressing"); - expect(downloadInitiator.callbackDispatcher.onDownloadProgress).not.toHaveBeenCalled(); + mockItem.pause(); + mockEmitter.emit("updated", "", "progressing"); + expect(downloadInitiator.callbackDispatcher.onDownloadProgress).not.toHaveBeenCalled(); - mockItem.resume(); - mockEmitter.emit("updated", "", "progressing"); - expect(downloadInitiator.callbackDispatcher.onDownloadProgress).toHaveBeenCalled(); - }) + mockItem.resume(); + mockEmitter.emit("updated", "", "progressing"); + expect(downloadInitiator.callbackDispatcher.onDownloadProgress).toHaveBeenCalled(); + }); }); }); diff --git a/test/utils.test.ts b/test/utils.test.ts index ba7da4b..8ac18a4 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -8,9 +8,17 @@ import { getFilenameFromMime, truncateUrl, } from "../src/utils"; +import { createMockDownloadData } from "../src/__mocks__/DownloadData"; jest.mock("electron"); +let mockedItemData; + +beforeEach(() => { + jest.clearAllMocks(); + mockedItemData = createMockDownloadData().item; +}); + describe("truncateUrl", () => { test("it should truncate URL if longer than 50 characters", () => { const url = "https://www.example.com/this/is/a/very/long/url/which/needs/truncation/to/maintain/50/characters"; @@ -105,11 +113,14 @@ describe("calculateDownloadMetrics", () => { }); it("calculates the download metrics correctly for positive elapsed time", () => { - const result = calculateDownloadMetrics({ - totalBytes: 5000, - downloadedBytes: 1000, - startTimeSecs: mockStartTimeSecs, - }); + mockedItemData.getReceivedBytes.mockReturnValue(1000); + mockedItemData.getTotalBytes.mockReturnValue(5000); + mockedItemData.getStartTime.mockReturnValue(mockStartTimeSecs); + + mockedItemData["getCurrentBytesPerSecond"] = undefined; + mockedItemData["getPercentComplete"] = undefined; + + const result = calculateDownloadMetrics(mockedItemData); expect(result).toEqual({ percentCompleted: 20, @@ -120,12 +131,14 @@ describe("calculateDownloadMetrics", () => { it("calculates zero download rate and estimated time if no time has elapsed", () => { const startTimeWithNoElapsedTime = 2000; // Mock current time is the same as start time + mockedItemData.getReceivedBytes.mockReturnValue(0); + mockedItemData.getTotalBytes.mockReturnValue(5000); + mockedItemData.getStartTime.mockReturnValue(startTimeWithNoElapsedTime); - const result = calculateDownloadMetrics({ - totalBytes: 5000, - downloadedBytes: 0, - startTimeSecs: startTimeWithNoElapsedTime, - }); + mockedItemData["getCurrentBytesPerSecond"] = undefined; + mockedItemData["getPercentComplete"] = undefined; + + const result = calculateDownloadMetrics(mockedItemData); expect(result).toEqual({ percentCompleted: 0, @@ -135,22 +148,43 @@ describe("calculateDownloadMetrics", () => { }); it("does not exceed 100% completion", () => { - const result = calculateDownloadMetrics({ - totalBytes: 2000, - downloadedBytes: 5000, // More bytes downloaded than total, which could be an error - startTimeSecs: mockStartTimeSecs, - }); + mockedItemData.getReceivedBytes.mockReturnValue(5000); + mockedItemData.getTotalBytes.mockReturnValue(2000); + mockedItemData.getStartTime.mockReturnValue(mockStartTimeSecs); + + mockedItemData["getCurrentBytesPerSecond"] = undefined; + mockedItemData["getPercentComplete"] = undefined; + + const result = calculateDownloadMetrics(mockedItemData); expect(result.percentCompleted).toBe(100); }); it("handles zero totalBytes without errors and returns zero for percentCompleted", () => { - const result = calculateDownloadMetrics({ - totalBytes: 0, - downloadedBytes: 1000, - startTimeSecs: mockStartTimeSecs, - }); + mockedItemData.getReceivedBytes.mockReturnValue(1000); + mockedItemData.getTotalBytes.mockReturnValue(0); + mockedItemData.getStartTime.mockReturnValue(mockStartTimeSecs); + + mockedItemData["getCurrentBytesPerSecond"] = undefined; + mockedItemData["getPercentComplete"] = undefined; + + const result = calculateDownloadMetrics(mockedItemData); expect(result.percentCompleted).toBe(0); }); + + describe("with getCurrentBytesPerSecond and getPercentComplete", () => { + it("calculates the download metrics correctly for positive elapsed time", () => { + mockedItemData.getCurrentBytesPerSecond.mockReturnValue(999); + mockedItemData.getPercentComplete.mockReturnValue(99); + + const result = calculateDownloadMetrics(mockedItemData); + + expect(result).toEqual({ + percentCompleted: 99, + downloadRateBytesPerSecond: 999, + estimatedTimeRemainingSeconds: expect.any(Number), + }); + }); + }); });