Repository: electron/electron
Pull request: feat:  add `getPercentComplete` / `getCurrentBytesPerSecond` / `getEndTime` to DownloadItem

#### Description of Change

This adds the following methods to `DownloadItem`:

- `getCurrentBytesPerSecond()`
- `getPercentComplete()`
- `getEndTime()`

Referenced via:

https://source.chromium.org/chromium/chromium/src/+/main:components/download/internal/common/download_item_impl.cc;bpv=0;bpt=1

#### Checklist

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [x] relevant documentation, tutorials, templates and examples are changed or added
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Added `DownloadItem.getCurrentBytesPerSecond()`, `DownloadItem.getPercentComplete()`, `DownloadItem.getEndTime()`.


Changed files:
filename: docs/api/download-item.md
patch: @@ -145,6 +145,10 @@ Returns `string` - The file name of the download item.
 disk. If user changes the file name in a prompted download saving dialog, the
 actual name of saved file will be different.
 
+#### `downloadItem.getCurrentBytesPerSecond()`
+
+Returns `Integer` - The current download speed in bytes per second.
+
 #### `downloadItem.getTotalBytes()`
 
 Returns `Integer` - The total size in bytes of the download item.
@@ -155,6 +159,10 @@ If the size is unknown, it returns 0.
 
 Returns `Integer` - The received bytes of the download item.
 
+#### `downloadItem.getPercentComplete()`
+
+Returns `Integer` - The download completion in percent.
+
 #### `downloadItem.getContentDisposition()`
 
 Returns `string` - The Content-Disposition field from the response
@@ -184,6 +192,10 @@ Returns `string` - ETag header value.
 Returns `Double` - Number of seconds since the UNIX epoch when the download was
 started.
 
+#### `downloadItem.getEndTime()`
+
+Returns `Double` - Number of seconds since the UNIX epoch when the download ended.
+
 ### Instance Properties
 
 #### `downloadItem.savePath`

filename: shell/browser/api/electron_api_download_item.cc
patch: @@ -149,6 +149,12 @@ void DownloadItem::Cancel() {
   download_item_->Cancel(true);
 }
 
+int64_t DownloadItem::GetCurrentBytesPerSecond() const {
+  if (!CheckAlive())
+    return 0;
+  return download_item_->CurrentSpeed();
+}
+
 int64_t DownloadItem::GetReceivedBytes() const {
   if (!CheckAlive())
     return 0;
@@ -161,6 +167,12 @@ int64_t DownloadItem::GetTotalBytes() const {
   return download_item_->GetTotalBytes();
 }
 
+int DownloadItem::GetPercentComplete() const {
+  if (!CheckAlive())
+    return 0;
+  return download_item_->PercentComplete();
+}
+
 std::string DownloadItem::GetMimeType() const {
   if (!CheckAlive())
     return "";
@@ -248,6 +260,12 @@ double DownloadItem::GetStartTime() const {
   return download_item_->GetStartTime().InSecondsFSinceUnixEpoch();
 }
 
+double DownloadItem::GetEndTime() const {
+  if (!CheckAlive())
+    return 0;
+  return download_item_->GetEndTime().InSecondsFSinceUnixEpoch();
+}
+
 // static
 gin::ObjectTemplateBuilder DownloadItem::GetObjectTemplateBuilder(
     v8::Isolate* isolate) {
@@ -258,8 +276,11 @@ gin::ObjectTemplateBuilder DownloadItem::GetObjectTemplateBuilder(
       .SetMethod("resume", &DownloadItem::Resume)
       .SetMethod("canResume", &DownloadItem::CanResume)
       .SetMethod("cancel", &DownloadItem::Cancel)
+      .SetMethod("getCurrentBytesPerSecond",
+                 &DownloadItem::GetCurrentBytesPerSecond)
       .SetMethod("getReceivedBytes", &DownloadItem::GetReceivedBytes)
       .SetMethod("getTotalBytes", &DownloadItem::GetTotalBytes)
+      .SetMethod("getPercentComplete", &DownloadItem::GetPercentComplete)
       .SetMethod("getMimeType", &DownloadItem::GetMimeType)
       .SetMethod("hasUserGesture", &DownloadItem::HasUserGesture)
       .SetMethod("getFilename", &DownloadItem::GetFilename)
@@ -276,7 +297,8 @@ gin::ObjectTemplateBuilder DownloadItem::GetObjectTemplateBuilder(
       .SetMethod("getSaveDialogOptions", &DownloadItem::GetSaveDialogOptions)
       .SetMethod("getLastModifiedTime", &DownloadItem::GetLastModifiedTime)
       .SetMethod("getETag", &DownloadItem::GetETag)
-      .SetMethod("getStartTime", &DownloadItem::GetStartTime);
+      .SetMethod("getStartTime", &DownloadItem::GetStartTime)
+      .SetMethod("getEndTime", &DownloadItem::GetEndTime);
 }
 
 const char* DownloadItem::GetTypeName() {

filename: shell/browser/api/electron_api_download_item.h
patch: @@ -62,8 +62,10 @@ class DownloadItem : public gin::Wrappable<DownloadItem>,
   void Resume();
   bool CanResume() const;
   void Cancel();
+  int64_t GetCurrentBytesPerSecond() const;
   int64_t GetReceivedBytes() const;
   int64_t GetTotalBytes() const;
+  int GetPercentComplete() const;
   std::string GetMimeType() const;
   bool HasUserGesture() const;
   std::string GetFilename() const;
@@ -76,6 +78,7 @@ class DownloadItem : public gin::Wrappable<DownloadItem>,
   std::string GetLastModifiedTime() const;
   std::string GetETag() const;
   double GetStartTime() const;
+  double GetEndTime() const;
 
   base::FilePath save_path_;
   file_dialog::DialogSettings dialog_options_;

filename: spec/api-session-spec.ts
patch: @@ -889,13 +889,21 @@ describe('session module', () => {
           }
         });
 
+        const today = Math.floor(Date.now() / 1000);
         const item = await downloadDone;
         expect(item.getState()).to.equal('completed');
         expect(item.getFilename()).to.equal('mock.pdf');
         expect(item.getMimeType()).to.equal('application/pdf');
         expect(item.getReceivedBytes()).to.equal(mockPDF.length);
         expect(item.getTotalBytes()).to.equal(mockPDF.length);
+        expect(item.getPercentComplete()).to.equal(100);
+        expect(item.getCurrentBytesPerSecond()).to.equal(0);
         expect(item.getContentDisposition()).to.equal(contentDisposition);
+
+        const start = item.getStartTime();
+        const end = item.getEndTime();
+        expect(start).to.be.greaterThan(today);
+        expect(end).to.be.greaterThan(start);
       });
 
       it('throws when called with invalid headers', () => {