mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-07-29 18:25:47 +02:00
Feat: Implement download cancellation feature with stop button and progress handling
This commit is contained in:
@@ -253,13 +253,14 @@
|
|||||||
<div id="downloadProgressWrapper">
|
<div id="downloadProgressWrapper">
|
||||||
<div id="downloadProgress"></div>
|
<div id="downloadProgress"></div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 20px;">
|
<div style="display: flex; gap: 20px; align-items: center;">
|
||||||
<div id="downloadProgressText"></div>
|
<div id="downloadProgressText"></div>
|
||||||
<div id="downloadSpeedText"></div>
|
<div id="downloadSpeedText"></div>
|
||||||
|
<button type="button" id="stopBtn" onclick="stopDownload()" class="stop-btn" style="display:none;" title="Stop download">■</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="downloadStatus"></div>
|
<div id="downloadStatus" style="display: none;"></div>
|
||||||
|
|
||||||
<div class="infos-container">
|
<div class="infos-container">
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
const form = document.getElementById("downloadForm");
|
const form = document.getElementById("downloadForm");
|
||||||
const statusDiv = document.getElementById("downloadStatus");
|
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) => {
|
form.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
button.disabled = true; // Avoid multiple clicks
|
button.disabled = true;
|
||||||
statusDiv.textContent = "Download in progress...";
|
stopBtn.style.display = "inline-flex";
|
||||||
|
statusDiv.style.display = "none"; // Hide status div
|
||||||
|
isDownloading = true;
|
||||||
|
userStoppedDownload = false;
|
||||||
|
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const params = new URLSearchParams(formData);
|
const params = new URLSearchParams(formData);
|
||||||
@@ -18,20 +24,51 @@ form.addEventListener("submit", async (e) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
statusDiv.textContent = res;
|
if (!userStoppedDownload) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
statusDiv.textContent = errorText;
|
||||||
|
statusDiv.style.display = "block";
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = await res.text();
|
} catch (err) {
|
||||||
statusDiv.textContent = text;
|
if (!isDownloading && !userStoppedDownload) {
|
||||||
|
statusDiv.style.display = "none";
|
||||||
} catch {
|
}
|
||||||
statusDiv.textContent = "❌ An Error has Occured.";
|
|
||||||
} finally {
|
} finally {
|
||||||
|
isDownloading = false;
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
|
stopBtn.style.display = "none";
|
||||||
setTimeout(() => {
|
|
||||||
statusDiv.textContent = "";
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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%";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,10 @@ function startProgress() {
|
|||||||
|
|
||||||
speedElement.style.display = "block";
|
speedElement.style.display = "block";
|
||||||
speedElement.innerHTML = "0 Mib/s";
|
speedElement.innerHTML = "0 Mib/s";
|
||||||
|
|
||||||
|
// Show stop button
|
||||||
|
const stopBtn = document.getElementById("stopBtn");
|
||||||
|
if (stopBtn) stopBtn.style.display = "inline-block";
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProgress(percent) {
|
function updateProgress(percent) {
|
||||||
@@ -28,6 +32,10 @@ function resetProgress() {
|
|||||||
|
|
||||||
speedElement.textContent = "0 Mib/s";
|
speedElement.textContent = "0 Mib/s";
|
||||||
speedElement.style.display = "none";
|
speedElement.style.display = "none";
|
||||||
|
|
||||||
|
// Hide stop button
|
||||||
|
const stopBtn = document.getElementById("stopBtn");
|
||||||
|
if (stopBtn) stopBtn.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSE Connexion
|
// SSE Connexion
|
||||||
|
|||||||
@@ -51,3 +51,39 @@
|
|||||||
font-size: 0.7rem;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { fetchDownload } = require("../services/download.services");
|
const { fetchDownload, cancelDownload } = require("../services/download.services");
|
||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger");
|
||||||
const { isValidUrl, isSafePath } = require("../helpers/validation.helpers");
|
const { isValidUrl, isSafePath } = require("../helpers/validation.helpers");
|
||||||
const { notifyDownloadFinished } = require("../helpers/notify.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};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
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("/", downloadController);
|
||||||
|
router.post("/cancel", cancelDownloadController);
|
||||||
|
|
||||||
// SSE for download progress
|
// SSE for download progress
|
||||||
router.get("/progress", progressController);
|
router.get("/progress", progressController);
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const notify = require("../helpers/notify.helpers");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { isSafePath } = require("../helpers/validation.helpers");
|
const { isSafePath } = require("../helpers/validation.helpers");
|
||||||
|
|
||||||
|
let currentDownloadProcess = null;
|
||||||
|
|
||||||
function fetchDownload(options, listeners, speedListeners) {
|
function fetchDownload(options, listeners, speedListeners) {
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -33,12 +35,15 @@ function fetchDownload(options, listeners, speedListeners) {
|
|||||||
logger.info(`[yt-dlp args] ${args.join(" ")}`);
|
logger.info(`[yt-dlp args] ${args.join(" ")}`);
|
||||||
|
|
||||||
const child = execFile(userYtDlp, args);
|
const child = execFile(userYtDlp, args);
|
||||||
|
currentDownloadProcess = child;
|
||||||
|
|
||||||
child.on("error", err => reject(new Error(`YT-DLP Error : ${err.message}`)));
|
child.on("error", err => reject(new Error(`YT-DLP Error : ${err.message}`)));
|
||||||
|
|
||||||
child.on("close", code => {
|
child.on("close", code => {
|
||||||
|
currentDownloadProcess = null;
|
||||||
listeners.forEach(fn => fn("done"));
|
listeners.forEach(fn => fn("done"));
|
||||||
if (code === 0) resolve(safeOutputFolder);
|
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}`));
|
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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user