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 };