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?" }