Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for new Electron DownloadItem APIs #10

Merged
merged 1 commit into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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),
});
});
});
});
Loading