Skip to content

Commit

Permalink
Add support for new Electron DownloadItem APIs (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
theogravity authored Jul 23, 2024
1 parent 7d5fcb9 commit c5a1e53
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 80 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
24 changes: 8 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
13 changes: 5 additions & 8 deletions src/DownloadInitiator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/__mocks__/DownloadData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
39 changes: 20 additions & 19 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
30 changes: 15 additions & 15 deletions test/DownloadInitiator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
74 changes: 54 additions & 20 deletions test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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),
});
});
});
});

0 comments on commit c5a1e53

Please sign in to comment.