diff --git a/html/playlist.html b/html/playlist.html
index 3510478..830f320 100644
--- a/html/playlist.html
+++ b/html/playlist.html
@@ -28,6 +28,17 @@
bottom: 8px;
}
+ #stopDownload {
+ display: none;
+ position: relative;
+ bottom: 8px;
+ background-color: var(--redBtn);
+ }
+
+ #stopDownload:active {
+ background-color: var(--redBtn-bottom);
+ }
+
#audioBox {
display: none;
}
@@ -187,6 +198,7 @@
+
diff --git a/src/playlist.js b/src/playlist.js
index 68dcf44..85bf9af 100644
--- a/src/playlist.js
+++ b/src/playlist.js
@@ -18,6 +18,9 @@ const playlistDownloader = {
playlistName: "",
originalCount: 0,
currentDownloadProcess: null,
+ currentAbortController: null,
+ isDownloading: false,
+ isCancelled: false,
},
config: {
@@ -53,6 +56,7 @@ const playlistDownloader = {
selectLocationBtn: document.getElementById("selectLocation"),
pathDisplay: document.getElementById("path"),
openDownloadsBtn: document.getElementById("openDownloads"),
+ stopDownloadBtn: document.getElementById("stopDownload"),
videoToggle: document.getElementById("videoToggle"),
audioToggle: document.getElementById("audioToggle"),
@@ -150,7 +154,9 @@ const playlistDownloader = {
document.activeElement.tagName !== "INPUT" &&
document.activeElement.tagName !== "TEXTAREA")
) {
- this.pasteLink();
+ if (!this.state.isDownloading) {
+ this.pasteLink();
+ }
}
});
@@ -185,6 +191,9 @@ const playlistDownloader = {
this.ui.openDownloadsBtn.addEventListener("click", () =>
this.openDownloadsFolder(),
);
+ this.ui.stopDownloadBtn.addEventListener("click", () =>
+ this.stopDownload(),
+ );
this.ui.closeHiddenBtn.addEventListener("click", () =>
this.hideOptions(true),
);
@@ -214,6 +223,8 @@ const playlistDownloader = {
},
async startDownload(type) {
+ if (this.state.isDownloading) return;
+
try {
this.state.url = this.validateUrl(this.state.url);
} catch (_) {
@@ -222,49 +233,66 @@ const playlistDownloader = {
return;
}
- await this.updateDynamicConfig();
- this.hideOptions();
-
- const controller = new AbortController();
- const baseArgs = this.buildBaseArgs();
- let specificArgs = [];
+ try {
+ await this.updateDynamicConfig();
+ this.hideOptions();
+
+ this.state.isDownloading = true;
+ this.state.isCancelled = false;
+
+ this.state.currentAbortController = new AbortController();
+ const baseArgs = this.buildBaseArgs();
+ let specificArgs = [];
+
+ switch (type) {
+ case "video":
+ specificArgs = this.getVideoArgs();
+ break;
+ case "audio":
+ specificArgs = this.getAudioArgs();
+ break;
+ case "thumbnails":
+ specificArgs = this.getThumbnailArgs();
+ break;
+ case "links":
+ specificArgs = this.getLinkArgs();
+ break;
+ }
- switch (type) {
- case "video":
- specificArgs = this.getVideoArgs();
- break;
- case "audio":
- specificArgs = this.getAudioArgs();
- break;
- case "thumbnails":
- specificArgs = this.getThumbnailArgs();
- break;
- case "links":
- specificArgs = this.getLinkArgs();
- break;
- }
+ const allArgs = [...baseArgs, ...specificArgs, this.state.url].filter(
+ Boolean,
+ );
- const allArgs = [...baseArgs, ...specificArgs, this.state.url].filter(
- Boolean,
- );
+ console.log(`Command: ${this.state.ytDlpPath}`, allArgs.join(" "));
+ this.state.currentDownloadProcess = this.state.ytDlpWrap.exec(
+ allArgs,
+ {shell: false},
+ this.state.currentAbortController.signal,
+ );
- console.log(`Command: ${this.state.ytDlpPath}`, allArgs.join(" "));
- this.state.currentDownloadProcess = this.state.ytDlpWrap.exec(
- allArgs,
- {shell: false},
- controller.signal,
- );
- document.addEventListener("beforeunload", () => {
- if (
- this.state.currentDownloadProcess &&
- !this.state.currentDownloadProcess.killed
- ) {
- this.state.currentDownloadProcess.kill();
- }
- });
+ // TODO: Avoid duplication of event listeners
+ document.addEventListener("beforeunload", () => {
+ if (this.state.currentAbortController) {
+ try {
+ this.state.currentAbortController.abort();
+ } catch (e) {}
+ }
+ if (
+ this.state.currentDownloadProcess &&
+ this.state.currentDownloadProcess.ytDlpProcess &&
+ !this.state.currentDownloadProcess.ytDlpProcess.killed
+ ) {
+ try {
+ this.state.currentDownloadProcess.ytDlpProcess.kill();
+ } catch (e) {}
+ }
+ });
- this.handleDownloadEvents(this.state.currentDownloadProcess, type);
+ this.handleDownloadEvents(this.state.currentDownloadProcess, type);
+ } catch (error) {
+ this.showError(error);
+ }
},
buildBaseArgs() {
@@ -481,7 +509,29 @@ const playlistDownloader = {
process.on("close", () => this.finishDownload(count));
},
+ stopDownload() {
+ this.state.isCancelled = true;
+ if (this.state.currentAbortController) {
+ try {
+ this.state.currentAbortController.abort();
+ } catch (e) {
+ console.error("Failed to abort controller:", e);
+ }
+ }
+ if (
+ this.state.currentDownloadProcess &&
+ this.state.currentDownloadProcess.ytDlpProcess
+ ) {
+ try {
+ this.state.currentDownloadProcess.ytDlpProcess.kill();
+ } catch (e) {
+ console.error("Failed to kill ytDlpProcess:", e);
+ }
+ }
+ },
+
pasteLink() {
+ if (this.state.isDownloading) return;
this.state.url = clipboard.readText();
this.ui.linkDisplay.textContent = ` ${this.state.url}`;
this.ui.optionsContainer.style.display = "block";
@@ -571,6 +621,7 @@ const playlistDownloader = {
this.ui.errorDetails.style.display = "none";
this.ui.errorDetails.textContent = "";
this.ui.errorMsgDisplay.style.display = "none";
+ this.ui.stopDownloadBtn.style.display = "none";
if (!justHide) {
this.ui.playlistNameDisplay.textContent = `${window.i18n.__(
@@ -578,10 +629,26 @@ const playlistDownloader = {
)}...`;
this.ui.pasteLinkBtn.style.display = "none";
this.ui.openDownloadsBtn.style.display = "inline-block";
+ this.ui.stopDownloadBtn.style.display = "inline-block";
}
},
finishDownload(count) {
+ if (!this.state.isDownloading) return;
+ this.state.isDownloading = false;
+ this.ui.stopDownloadBtn.style.display = "none";
+
+ if (this.state.isCancelled) {
+ this.state.isCancelled = false;
+ const lastProgress = document.getElementById(`p${count}`);
+ if (lastProgress) {
+ lastProgress.textContent = window.i18n.__("cancel");
+ }
+ this.ui.playlistNameDisplay.textContent = window.i18n.__("cancel");
+ this.ui.pasteLinkBtn.style.display = "inline-block";
+ return;
+ }
+
const lastProgress = document.getElementById(`p${count}`);
if (lastProgress)
lastProgress.textContent = window.i18n.__("fileSaved");
@@ -597,6 +664,17 @@ const playlistDownloader = {
},
showError(error) {
+ const wasDownloading = this.state.isDownloading;
+ this.state.isDownloading = false;
+ this.ui.stopDownloadBtn.style.display = "none";
+
+ if (this.state.isCancelled && wasDownloading) {
+ this.state.isCancelled = false;
+ this.ui.playlistNameDisplay.textContent = window.i18n.__("cancel");
+ this.ui.pasteLinkBtn.style.display = "inline-block";
+ return;
+ }
+
console.error("Download Error:", error.toString());
this.ui.pasteLinkBtn.style.display = "inline-block";
this.ui.openDownloadsBtn.style.display = "none";
@@ -663,6 +741,14 @@ const playlistDownloader = {
},
navigate(type, page) {
+ if (this.state.isDownloading) {
+ const confirmMsg = window.i18n ? window.i18n.__("cancel_download") + "?" : "Are you sure you want to cancel the download?";
+ const choice = confirm(confirmMsg);
+ if (!choice) {
+ return;
+ }
+ this.stopDownload();
+ }
this.closeMenu();
const event = type === "page" ? "load-page" : "load-win";
ipcRenderer.send(event, path.join(__dirname, page));
diff --git a/translations/en.json b/translations/en.json
index 70a6610..6b3e5db 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -181,9 +181,9 @@
"qualityTargetSizeDesc": "Specify target MB",
"targetSizeMB": "Target Size (MB)",
"worst": "Worst",
- "best": "Best",
"advancedSettings": "Advanced Settings",
"qualityTargetPercent": "Target %",
"qualityTargetPercentDesc": "Set target % of size",
- "targetPercentLabel": "Target Percentage (%)"
+ "targetPercentLabel": "Target Percentage (%)",
+ "cancel_download": "Are you sure you want to cancel the download?"
}