From 60137a15dc4094e4a7107c2aff0e363ae50be645 Mon Sep 17 00:00:00 2001
From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com>
Date: Sun, 12 Apr 2026 11:22:18 +0200
Subject: [PATCH] Feat: Implement download cancellation feature with stop
button and progress handling
---
public/index.html | 5 +-
public/script/downloadstatus.js | 65 +++++++++++++++++++-----
public/script/progressBar.js | 8 +++
public/styles/components/progressBar.css | 36 +++++++++++++
server/controller/download.controller.js | 14 ++++-
server/routes/download.route.js | 3 +-
server/services/download.services.js | 34 ++++++++++++-
7 files changed, 145 insertions(+), 20 deletions(-)
diff --git a/public/index.html b/public/index.html
index 9581bb2..fd147e0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -253,13 +253,14 @@
-
-
+
diff --git a/public/script/downloadstatus.js b/public/script/downloadstatus.js
index e6818b0..add6b71 100644
--- a/public/script/downloadstatus.js
+++ b/public/script/downloadstatus.js
@@ -1,11 +1,17 @@
const form = document.getElementById("downloadForm");
const statusDiv = document.getElementById("downloadStatus");
-const button = form.querySelector("button");
+const button = form.querySelector("button[type=\"submit\"]");
+const stopBtn = document.getElementById("stopBtn");
+let isDownloading = false;
+let userStoppedDownload = false;
form.addEventListener("submit", async (e) => {
e.preventDefault();
- button.disabled = true; // Avoid multiple clicks
- statusDiv.textContent = "Download in progress...";
+ button.disabled = true;
+ stopBtn.style.display = "inline-flex";
+ statusDiv.style.display = "none"; // Hide status div
+ isDownloading = true;
+ userStoppedDownload = false;
const formData = new FormData(form);
const params = new URLSearchParams(formData);
@@ -18,20 +24,51 @@ form.addEventListener("submit", async (e) => {
});
if (!res.ok) {
- statusDiv.textContent = res;
+ if (!userStoppedDownload) {
+ const errorText = await res.text();
+ statusDiv.textContent = errorText;
+ statusDiv.style.display = "block";
+ }
return;
}
- const text = await res.text();
- statusDiv.textContent = text;
-
- } catch {
- statusDiv.textContent = "❌ An Error has Occured.";
+ } catch (err) {
+ if (!isDownloading && !userStoppedDownload) {
+ statusDiv.style.display = "none";
+ }
} finally {
+ isDownloading = false;
button.disabled = false;
-
- setTimeout(() => {
- statusDiv.textContent = "";
- }, 5000);
+ stopBtn.style.display = "none";
}
-});
\ No newline at end of file
+});
+
+async function stopDownload() {
+ if (!isDownloading) return;
+
+ userStoppedDownload = true;
+ button.disabled = true;
+ stopBtn.disabled = true;
+
+ try {
+ await fetch("/download/cancel", {
+ method: "POST",
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
+ });
+
+ } catch (err) {
+ console.error("Error stopping download:", err);
+ } finally {
+ isDownloading = false;
+ button.disabled = false;
+ stopBtn.disabled = false;
+ stopBtn.style.display = "none";
+
+ // Reset progress bar
+ const progressWrapper = document.getElementById("downloadProgressWrapper");
+ if (progressWrapper) progressWrapper.style.display = "none";
+
+ const progressBar = document.getElementById("downloadProgress");
+ if (progressBar) progressBar.style.width = "0%";
+ }
+}
\ No newline at end of file
diff --git a/public/script/progressBar.js b/public/script/progressBar.js
index 780a551..abe6958 100644
--- a/public/script/progressBar.js
+++ b/public/script/progressBar.js
@@ -13,6 +13,10 @@ function startProgress() {
speedElement.style.display = "block";
speedElement.innerHTML = "0 Mib/s";
+
+ // Show stop button
+ const stopBtn = document.getElementById("stopBtn");
+ if (stopBtn) stopBtn.style.display = "inline-block";
}
function updateProgress(percent) {
@@ -28,6 +32,10 @@ function resetProgress() {
speedElement.textContent = "0 Mib/s";
speedElement.style.display = "none";
+
+ // Hide stop button
+ const stopBtn = document.getElementById("stopBtn");
+ if (stopBtn) stopBtn.style.display = "none";
}
// SSE Connexion
diff --git a/public/styles/components/progressBar.css b/public/styles/components/progressBar.css
index 70a9630..a16d55f 100644
--- a/public/styles/components/progressBar.css
+++ b/public/styles/components/progressBar.css
@@ -51,3 +51,39 @@
font-size: 0.7rem;
}
}
+
+/* Stop Button */
+.stop-btn {
+ width: 40px;
+ height: 40px;
+ min-width: 40px;
+ padding: 0;
+ border: none;
+ border-radius: 8px;
+ background-color: var(--form-button-bg-color);
+ color: var(--form-button-text-color);
+ font-size: 20px;
+ font-weight: bold;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
+}
+
+.stop-btn:hover:not(:disabled) {
+ background-color: var(--form-button-bg-hover-color);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
+ transform: scale(1.15);
+}
+
+.stop-btn:active:not(:disabled) {
+ transform: scale(0.96);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
+}
+
+.stop-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
diff --git a/server/controller/download.controller.js b/server/controller/download.controller.js
index fe652ab..68deb6a 100644
--- a/server/controller/download.controller.js
+++ b/server/controller/download.controller.js
@@ -1,4 +1,4 @@
-const { fetchDownload } = require("../services/download.services");
+const { fetchDownload, cancelDownload } = require("../services/download.services");
const { logger } = require("../logger");
const { isValidUrl, isSafePath } = require("../helpers/validation.helpers");
const { notifyDownloadFinished } = require("../helpers/notify.helpers");
@@ -64,5 +64,15 @@ function speedController(req, res) {
});
}
+function cancelDownloadController(req, res) {
+ const cancelled = cancelDownload();
+ if (cancelled) {
+ logger.info("Download and queue cancelled by user");
+ res.send("✅ Download stopped! Queue cleared.");
+ } else {
+ logger.warn("No download to cancel");
+ res.status(400).send("❌ No download in progress !");
+ }
+}
-module.exports = { downloadController, progressController, speedController};
+module.exports = { downloadController, progressController, speedController, cancelDownloadController};
diff --git a/server/routes/download.route.js b/server/routes/download.route.js
index 14a9f3f..24267ab 100644
--- a/server/routes/download.route.js
+++ b/server/routes/download.route.js
@@ -1,8 +1,9 @@
const express = require("express");
const router = express.Router();
-const { downloadController, progressController, speedController } = require("../controller/download.controller");
+const { downloadController, progressController, speedController, cancelDownloadController } = require("../controller/download.controller");
router.post("/", downloadController);
+router.post("/cancel", cancelDownloadController);
// SSE for download progress
router.get("/progress", progressController);
diff --git a/server/services/download.services.js b/server/services/download.services.js
index 4ff58e7..5df2923 100644
--- a/server/services/download.services.js
+++ b/server/services/download.services.js
@@ -7,6 +7,8 @@ const notify = require("../helpers/notify.helpers");
const path = require("path");
const { isSafePath } = require("../helpers/validation.helpers");
+let currentDownloadProcess = null;
+
function fetchDownload(options, listeners, speedListeners) {
return new Promise((resolve, reject) => {
@@ -33,12 +35,15 @@ function fetchDownload(options, listeners, speedListeners) {
logger.info(`[yt-dlp args] ${args.join(" ")}`);
const child = execFile(userYtDlp, args);
+ currentDownloadProcess = child;
child.on("error", err => reject(new Error(`YT-DLP Error : ${err.message}`)));
child.on("close", code => {
+ currentDownloadProcess = null;
listeners.forEach(fn => fn("done"));
if (code === 0) resolve(safeOutputFolder);
+ else if (code === null) reject(new Error(`Download cancelled by user`));
else reject(new Error(`YT-DLP failed with code : ${code}`));
});
@@ -76,4 +81,31 @@ function fetchDownload(options, listeners, speedListeners) {
});
}
-module.exports = { fetchDownload };
+function cancelDownload() {
+ if (currentDownloadProcess) {
+ logger.info("Cancelling download and killing all child processes...");
+
+ // Force kill the process and all its children with SIGKILL
+ try {
+ // Try to kill with SIGKILL on Windows (process group) or Unix
+ if (process.platform === 'win32') {
+ // On Windows, kill the process tree
+ require('child_process').execSync(`taskkill /PID ${currentDownloadProcess.pid} /T /F`, { stdio: 'ignore' });
+ } else {
+ // On Unix, send SIGKILL to process group
+ process.kill(-currentDownloadProcess.pid, 'SIGKILL');
+ }
+ } catch (err) {
+ logger.warn(`Error killing process: ${err.message}`);
+ // Fallback to regular kill
+ currentDownloadProcess.kill('SIGKILL');
+ }
+
+ currentDownloadProcess = null;
+ logger.info("Download cancelled successfully");
+ return true;
+ }
+ return false;
+}
+
+module.exports = { fetchDownload, cancelDownload };