Feat: Enhance download experience with progress indicators and toast notifications

This commit is contained in:
MasterAcnolo
2026-04-12 12:04:05 +02:00
parent 5aebc78d44
commit 011e0a703a
11 changed files with 329 additions and 31 deletions

View File

@@ -5,6 +5,7 @@ const { notifyDownloadFinished } = require("../helpers/notify.helpers");
const listeners = [];
const speedListeners = [];
const stageListeners = [];
async function downloadController(req, res) {
try {
@@ -15,16 +16,16 @@ async function downloadController(req, res) {
outputFolder: req.body.savePath || undefined,
};
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("Invalid URL !");
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("Invalid URL !");
if (options.outputFolder && !isSafePath(options.outputFolder)) {
logger.warn(`Unsafe download path rejected: ${options.outputFolder}`);
return res.status(400).send("❌ Save Path Not Allowed.");
}
// Get output folder when the download is finished
const outputFolder = await fetchDownload(options, listeners, speedListeners);
const outputFolder = await fetchDownload(options, listeners, speedListeners, stageListeners);
notifyDownloadFinished(outputFolder);
res.send("Download Done !");
res.send("Download Done !");
} catch (err) {
logger.error(`Server Error in /download : ${err.message}`);
@@ -64,6 +65,22 @@ function speedController(req, res) {
});
}
function stageController(req, res) {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const sendStage = stage => res.write(`data: ${stage}\n\n`);
stageListeners.push(sendStage);
req.on('close', () => {
stageListeners.splice(stageListeners.indexOf(sendStage), 1);
res.end();
});
}
function cancelDownloadController(req, res) {
const cancelled = cancelDownload();
if (cancelled) {
@@ -75,4 +92,4 @@ function cancelDownloadController(req, res) {
}
}
module.exports = { downloadController, progressController, speedController, cancelDownloadController};
module.exports = { downloadController, progressController, speedController, stageController, cancelDownloadController};

View File

@@ -10,7 +10,7 @@ async function infoController(req, res){
// If no url, non-string url or invalid url
if (!url || typeof url !== "string" || !isValidUrl(url)) return res.status(400).send("Invalid URL Or Missing");
if (!url || typeof url !== "string" || !isValidUrl(url)) return res.status(400).send("Invalid URL Or Missing");
logger.info(`/Info Request receive by the Info Controller. URL: ${url}`);

View File

@@ -1,6 +1,6 @@
const express = require("express");
const router = express.Router();
const { downloadController, progressController, speedController, cancelDownloadController } = require("../controller/download.controller");
const { downloadController, progressController, speedController, stageController, cancelDownloadController } = require("../controller/download.controller");
router.post("/", downloadController);
router.post("/cancel", cancelDownloadController);
@@ -8,5 +8,6 @@ router.post("/cancel", cancelDownloadController);
// SSE for download progress
router.get("/progress", progressController);
router.get("/speed", speedController);
router.get("/stage", stageController);
module.exports = router;

View File

@@ -9,7 +9,7 @@ const { isSafePath } = require("../helpers/validation.helpers");
let currentDownloadProcess = null;
function fetchDownload(options, listeners, speedListeners) {
function fetchDownload(options, listeners, speedListeners, stageListeners) {
return new Promise((resolve, reject) => {
const outputFolder = options.outputFolder || defaultDownloadFolder;
@@ -54,7 +54,10 @@ function fetchDownload(options, listeners, speedListeners) {
logger.info(`[yt-dlp] ${line}`);
// Progress Bar
if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset"));
if (line.startsWith("[download] Destination:")) {
listeners.forEach(fn => fn("reset"));
stageListeners.forEach(fn => fn("Downloading..."));
}
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
if (match) listeners.forEach(fn => fn(parseFloat(match[1])));
@@ -66,6 +69,23 @@ function fetchDownload(options, listeners, speedListeners) {
}
}
// Stage messages
if (line.includes("[Merger]")) {
stageListeners.forEach(fn => fn("Merging formats..."));
}
if (line.includes("[ExtractAudio]")) {
stageListeners.forEach(fn => fn("Extracting audio..."));
}
if (line.includes("[Metadata]")) {
stageListeners.forEach(fn => fn("Adding metadata..."));
}
if (line.includes("[ThumbnailsConvertor]")) {
stageListeners.forEach(fn => fn("Converting thumbnail..."));
}
if (line.includes("[EmbedThumbnail]")) {
stageListeners.forEach(fn => fn("Embedding thumbnail..."));
}
if (line.match(/ERROR: Could not copy .* cookie database/i)) {
notify.notifyCookiesBrowserError();
}