refactor: add full documentation and fix theme loading robustness

- Add JSDoc comments across frontend and backend modules
- Improve code readability and maintainability with consistent documentation style
- Document Electron IPC handlers, UI utilities, and theme system
- Add missing function/class documentation in theme loader and app services
- Minor structural improvements and cleanup across Electron main process modules
- Minor fixes on variable names
This commit is contained in:
MasterAcnolo
2026-05-31 20:38:42 +02:00
parent 6afc5e06d6
commit 0218664c5a
34 changed files with 1777 additions and 340 deletions

View File

@@ -4,11 +4,47 @@ const { isValidUrl, isSafePath } = require("../helpers/validation.helpers");
const { notifyDownloadFinished } = require("../helpers/notify.helpers");
const { configFeatures } = require("../../config");
const listeners = [];
const speedListeners = [];
const stageListeners = [];
const playlistInfoListeners = [];
/**
* Active Server-Sent Event subscribers receiving
* download progress updates.
*
* Each listener receives a percentage value between
* 0 and 100 during the download lifecycle.
*/
const progressListeners = [];
/**
* Active Server-Sent Event subscribers receiving
* download speed updates.
*/
const speedListeners = [];
/**
* Active Server-Sent Event subscribers receiving
* download stage updates.
*/
const stageListeners = [];
/**
* Active Server-Sent Event subscribers receiving
* playlist progress information.
*/
const playlistListeners = [];
/**
* Handles download requests and starts a yt-dlp download.
*
* Validates user input, prepares download options and
* delegates the download process to the download service.
*
* Once the download completes successfully, a desktop
* notification may be displayed depending on the
* application configuration.
*
* @param {import("express").Request} req
* @param {import("express").Response} res
* @returns {Promise<void>}
*/
async function downloadController(req, res) {
try {
const options = {
@@ -25,17 +61,30 @@ async function downloadController(req, res) {
return res.status(400).send("Save Path Not Allowed.");
}
// Get output folder when the download is finished
const outputFolder = await fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners);
const outputFolder = await fetchDownload(
options,
progressListeners,
speedListeners,
stageListeners,
playlistListeners
);
notifyDownloadFinished(outputFolder, configFeatures.notifySystem);
res.send("Download Done !");
} catch (err) {
logger.error(`Server Error in /download : ${err.message}`);
res.status(500).send(`Server Error`);
logger.error(`Server Error in /download: ${err?.message ?? err}`);
res.status(500).send("Server Error");
}
}
/**
* Opens a Server-Sent Events (SSE) connection used to
* stream download progress updates to the client.
*
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
function progressController(req, res) {
res.set({
'Content-Type': 'text/event-stream',
@@ -44,14 +93,21 @@ function progressController(req, res) {
});
const sendProgress = percent => res.write(`data: ${percent}\n\n`);
listeners.push(sendProgress);
progressListeners.push(sendProgress);
req.on('close', () => {
listeners.splice(listeners.indexOf(sendProgress), 1);
progressListeners.splice(progressListeners.indexOf(sendProgress), 1);
res.end();
});
}
/**
* Opens a Server-Sent Events (SSE) connection used to
* stream download speed updates to the client.
*
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
function speedController(req, res) {
res.set({
'Content-Type': 'text/event-stream',
@@ -68,6 +124,14 @@ function speedController(req, res) {
});
}
/**
* Opens a Server-Sent Events (SSE) connection used to
* stream download stage updates such as downloading,
* merging, extracting audio or embedding metadata.
*
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
function stageController(req, res) {
res.set({
'Content-Type': 'text/event-stream',
@@ -84,6 +148,14 @@ function stageController(req, res) {
});
}
/**
* Cancels the currently running download process.
*
* Returns an error response if no download is active.
*
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
function cancelDownloadController(req, res) {
const cancelled = cancelDownload();
if (cancelled) {
@@ -95,6 +167,18 @@ function cancelDownloadController(req, res) {
}
}
/**
* Opens a Server-Sent Events (SSE) connection used to
* stream playlist progress information to the client.
*
* Example:
* - Item 1 of 20
* - Item 12 of 20
* - Item 20 of 20
*
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
function playlistInfoController(req, res) {
res.set({
'Content-Type': 'text/event-stream',
@@ -103,10 +187,10 @@ function playlistInfoController(req, res) {
});
const sendPlaylistInfo = info => res.write(`data: ${info}\n\n`);
playlistInfoListeners.push(sendPlaylistInfo);
playlistListeners.push(sendPlaylistInfo);
req.on('close', () => {
playlistInfoListeners.splice(playlistInfoListeners.indexOf(sendPlaylistInfo), 1);
playlistListeners.splice(playlistListeners.indexOf(sendPlaylistInfo), 1);
res.end();
});
}

View File

@@ -3,70 +3,114 @@ const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers");
const { logger } = require("../logger");
const { isValidUrl } = require("../helpers/validation.helpers");
async function infoController(req, res){
/**
* Handles metadata retrieval for a video or playlist.
*
* This controller:
* - Validates incoming URL (query or body)
* - Calls yt-dlp metadata extraction service
* - Detects playlist vs single video context
* - Applies fallback strategy if playlist extraction fails
* - Returns normalized parsed response (video or playlist)
*
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
async function infoController(req, res) {
const url = req.body.url || req.query.url; // POST et GET
const url = req.body.url || req.query.url; // Supports POST and GET requests
// 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");
logger.info(`/Info Request receive by the Info Controller. URL: ${url}`);
if (url.includes("&list") || url.includes("?list")) {
logger.info("Estimated Data Type: Playlist")
} else{
logger.info("Estimated Data Type: Video")
/**
* Basic validation:
* - URL must exist
* - Must be a string
* - Must pass custom URL validation
*/
if (!url || typeof url !== "string" || !isValidUrl(url)) {
return res.status(400).send("Invalid URL Or Missing");
}
try {
let data;
let fetchError = null;
logger.info(`Info request received. URL: ${url}`);
// Lightweight heuristic to detect playlist URLs
const isPlaylistUrl = url.includes("&list") || url.includes("?list");
logger.info(
isPlaylistUrl
? "Estimated Data Type: Playlist"
: "Estimated Data Type: Video"
);
try {
data = await fetchInfo(url);
} catch (err) {
fetchError = err;
// If it's a playlist URL and fetch failed, try fetching just the single video
if ((url.includes("&list") || url.includes("?list"))) {
logger.info(`Playlist fetch failed: ${err.message}. Retrying with single video...`);
// Remove the &list parameter
const singleVideoUrl = url.split("&list")[0].split("?list")[0];
try {
data = await fetchInfo(singleVideoUrl);
logger.info(`Single video fetch succeeded after playlist failure`);
} catch (retryErr) {
logger.error(`Single video fetch also failed: ${retryErr.message}`);
throw err; // Throw the original error
let data;
/**
* Primary metadata fetch attempt
*/
try {
data = await fetchInfo(url);
} catch (err) {
/**
* Fallback strategy:
* If playlist extraction fails, retry as single video
*/
if (isPlaylistUrl) {
logger.info(
`Playlist fetch failed: ${err.message}. Retrying as single video...`
);
// Remove playlist query parameter (best-effort cleanup)
const singleVideoUrl = url.split("&list")[0].split("?list")[0];
try {
data = await fetchInfo(singleVideoUrl);
logger.info("Single video fetch succeeded after playlist failure");
} catch (retryErr) {
logger.error(
`Single video fetch also failed: ${retryErr.message}`
);
// Preserve original error context
throw err;
}
} else {
throw err;
}
} else {
throw err;
}
}
if (data._type === "playlist") {
/**
* Normalize response based on yt-dlp output type
*/
if (data._type === "playlist") {
const playlist = parsePlaylist(data);
logger.info(`Playlist : ${playlist.title} (${playlist.count} vidéos)`);
return res.json(playlist);
const playlist = parsePlaylist(data);
} else{
const video = parseVideo(data);
logger.info(`Unique Video: ${video.title}`);
return res.json(video);
}
logger.info(
`Playlist fetched: ${playlist.title} (${playlist.count} videos)`
);
return res.json(playlist);
} else {
const video = parseVideo(data);
logger.info(`Video fetched: ${video.title}`);
return res.json(video);
}
} catch (err) {
logger.error(`Info controller error: ${err && err.message ? err.message : err}`);
return res.status(500).send(`Unable to fetch info`);
logger.error(
`Info controller error: ${err && err.message ? err.message : err}`
);
return res.status(500).send("Unable to fetch info");
}
}
module.exports = {infoController};
module.exports = { infoController };

View File

@@ -5,6 +5,26 @@ const { ffmpegPath, denoPath} = require("./path.helpers.js");
const { configFeatures } = require("../../config.js");
const { logger } = require("../logger.js");
/**
* Validates whether the provided codec is supported by the application.
*
* If the codec is supported, it is returned unchanged.
* Otherwise, a warning is logged and the default codec ("h264")
* is returned as a fallback.
*
* Supported codecs:
* - av01
* - vp9.2
* - vp9
* - h265
* - h264
* - vp8
* - h263
* - theora
*
* @param {string} codec - Codec identifier to validate.
* @returns {string} A valid codec identifier.
*/
function validateCodec(codec){
const validCodec = [
@@ -20,6 +40,28 @@ function validateCodec(codec){
}
}
/**
* Builds the complete yt-dlp argument list based on the
* application configuration and download options.
*
* The generated arguments include:
* - Browser cookie extraction
* - FFmpeg integration
* - Deno runtime configuration
* - Metadata and thumbnail embedding
* - Playlist handling
* - Video/audio format selection
* - Quality filtering
* - Output file destination
*
* @param {Object} options - Download configuration.
* @param {string} options.url - Video or playlist URL.
* @param {boolean} options.audioOnly - Whether to extract audio only.
* @param {string} options.quality - Requested quality preset.
* @param {string} options.outputFolder - Download destination folder.
*
* @returns {string[]} Array of arguments ready to be passed to yt-dlp.
*/
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
logger.info("--- CONFIGURATION ---");

View File

@@ -4,10 +4,33 @@ const path = require("path");
const notify = require("./notify.helpers")
const { logger } = require("../logger");
/**
* Detects the first supported browser installed on the system.
*
* The detected browser is used by yt-dlp to import authentication
* cookies when required by certain platforms.
*
* Currently, Firefox is the only browser officially supported
* by the application.
*
* If no supported browser is found:
* - A warning is logged
* - A notification is displayed to the user
* - "firefox" is returned as a fallback to allow yt-dlp to
* handle the error gracefully without crashing the application
*
* @returns {string} The detected browser identifier.
*/
function getUserBrowser() {
const userProfile = os.homedir();
// List of browsers supported by yt-dlp (Currently I can only guarantee Firefox, Sorry)
/**
* Browsers supported by yt-dlp cookie extraction.
*
* Only Firefox is currently enabled and tested.
* Additional browsers can be enabled in the future
* once compatibility has been verified.
*/
const browsers = [
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
@@ -19,7 +42,7 @@ function getUserBrowser() {
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
];
// Search for an available browser
// Search for the first available browser profile.
for (const browser of browsers) {
if (fs.existsSync(browser.path)) {
logger.info(`Browser found: ${browser.name}`);
@@ -27,12 +50,12 @@ function getUserBrowser() {
}
}
// No browser found - notify user
// No supported browser found => Notify User
logger.warn("No supported browser found on the system");
notify.notifyFirefoxBrowserMissing();
// Fallback: return "Firefox" to let YT-DLP manage error
// this avoid app crash
// Fallback to Firefox and let yt-dlp handle the error gracefully.
// This prevents the application from crashing
return "firefox";
}

View File

@@ -1,8 +1,19 @@
const { Notification, shell } = require("electron");
const { iconPaths } = require("./path.helpers");
const { logger } = require("../logger");
/**
* Displays a system notification when a download completes successfully.
*
* If enabled, clicking the notification opens the download folder
* using the system file explorer.
*
* @param {string} folder - Path to the completed download folder.
* @param {boolean} notifyEnabled - Whether notifications are enabled in config.
*/
function notifyDownloadFinished(folder, notifyEnabled = true) {
if (!notifyEnabled) return;
if (!folder) return;
const notif = new Notification({
title: "Freedom Loader",
@@ -14,6 +25,15 @@ function notifyDownloadFinished(folder, notifyEnabled = true) {
notif.show();
}
/**
* Displays a notification when browser cookies cannot be extracted.
*
* This usually indicates:
* - User is not logged in to the browser
* - Browser profile is inaccessible
*
* Clicking the notification opens a tutorial link.
*/
function notifyCookiesBrowserError(){
const notif = new Notification({
title: "Cookies Error",
@@ -21,23 +41,41 @@ function notifyCookiesBrowserError(){
icon: iconPaths.error,
});
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"));
notif.on("click", () =>
shell.openExternal(
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
)
);
notif.show();
}
function notifyFirefoxBrowserMissing(){
/**
* Displays a notification when Firefox is not detected
* on the user system.
*
* This is required for cookie extraction via yt-dlp.
*
* Clicking the notification opens an installation guide.
*/
function notifyFirefoxBrowserMissing() {
const notif = new Notification({
title: "Firefox Missing",
body: "Firefox was not found on your system. Click here to follow the installation guide",
icon: iconPaths.error,
});
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"))
notif.on("click", () =>
shell.openExternal(
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
)
);
notif.show();
}
module.exports = {
notifyDownloadFinished,
notifyDownloadFinished,
notifyCookiesBrowserError,
notifyFirefoxBrowserMissing
notifyFirefoxBrowserMissing,
};

View File

@@ -1,27 +1,38 @@
/**
* Normalizes a video payload by attaching the resource type.
*
* @param {Object} data - Raw video metadata.
* @returns {Object} Normalized video object.
*/
function parseVideo(data) {
return {
type: "video",
...data
};
return {
type: "video",
...data
};
}
/**
* Transforms a raw playlist payload into a normalized structure
* containing playlist metadata and a simplified list of videos.
*
* @param {Object} data - Raw playlist metadata.
* @returns {Object} Normalized playlist object.
*/
function parsePlaylist(data) {
return {
type: "playlist",
title: data.title || data.id,
channel: data.uploader,
count: data.entries.length,
videos: (data.entries || []).map(v => ({
id: v.id,
title: v.title,
url: v.webpage_url,
duration: v.duration,
thumbnail: v.thumbnail,
uploader: v.uploader
}))
};
return {
type: "playlist",
title: data.title || data.id,
channel: data.uploader,
count: data.entries.length,
videos: (data.entries || []).map(v => ({
id: v.id,
title: v.title,
url: v.webpage_url,
duration: v.duration,
thumbnail: v.thumbnail,
uploader: v.uploader
}))
};
}
module.exports = { parseVideo, parsePlaylist };
module.exports = { parseVideo, parsePlaylist };

View File

@@ -14,13 +14,23 @@ const resourcesPath = config.devMode
// Default download folder (centralized)
const defaultDownloadFolder = path.join(os.homedir(), "Downloads", "Freedom Loader");
// Binary paths
/**
* Runtime-resolved binary paths.
* These values differ between development and production builds.
*/
let userYtDlp;
let ffmpegPath;
let denoPath;
/** Source binary bundled inside application resources (used for first-time copy) */
const sourceYtDlp = path.join(resourcesPath, "binaries","yt-dlp.exe");
/**
* Resolve binary locations depending on environment:
* - dev: local resources folder
* - prod: extracted userData + packaged resources
*/
if (config.devMode) {
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe");
ffmpegPath = path.join(__dirname, "../../ressources/"); // <- has ffmpeg.exe and ffprobe.exe
@@ -35,14 +45,20 @@ if (config.devMode) {
}
}
// Notification icon paths
/**
* Centralized notification and UI icon paths.
* Used by Electron notifications and UI components.
*/
const iconPaths = {
confirm: path.join(resourcesPath, "confirm-icon.png"),
error: path.join(resourcesPath, "error.png"),
app: path.join(resourcesPath, "app-icon.ico")
};
// Binary paths for verification
/**
* Static reference paths to bundled binaries.
* Used mainly for verification and diagnostics.
*/
const binaryPaths = {
ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"),
ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"),
@@ -50,10 +66,21 @@ const binaryPaths = {
deno: path.join(resourcesPath, "binaries", "deno.exe")
};
// Theme paths - themes stored in AppData to survive updates
/**
* Theme storage system:
* - default themes come from app resources
* - user themes are persisted in userData to survive updates
*/
const userThemesPath = path.join(app.getPath("userData"), "themes");
const defaultThemesSourcePath = path.join(resourcesPath, "theme");
/**
* Initializes user theme directory and ensures default themes exist.
*
* - Creates user themes folder if missing
* - Copies bundled themes from resources if not present
* - Applies permissive permissions for runtime modification
*/
function initUserThemes() {
try {
if (!fs.existsSync(userThemesPath)) {
@@ -94,6 +121,13 @@ function initUserThemes() {
}
}
/**
* Recursively copies a directory and its contents.
*
* @param {string} src - Source directory
* @param {string} dest - Destination directory
* @throws Error If copy operation fails
*/
function copyDirectory(src, dest) {
try {
if (!fs.existsSync(dest)) {
@@ -116,8 +150,9 @@ function copyDirectory(src, dest) {
}
}
if (!userYtDlp){ logger.error("Missing YT-DLP")}
if (!ffmpegPath){ logger.error("Missing FFMPEG")}
if (!denoPath){ logger.error("Missing DENO")}
// Runtime validation of critical binaries
if (!userYtDlp){ logger.error("Missing yt-dlp binary")}
if (!ffmpegPath){ logger.error("Missing ffmpeg binary")}
if (!denoPath){ logger.error("Missing deno binary")}
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder, userThemesPath, initUserThemes };

View File

@@ -1,14 +1,24 @@
const RateLimit = require("express-rate-limit");
/* Rate Limite */
const rateLimite = RateLimit({
/**
* Rate limiter middleware used to protect public endpoints
* against abusive or excessive requests.
*
* Restrictions:
* - 5 requests per IP address
* - 15-minute sliding window
*
* When the limit is exceeded, a HTTP 429 (Too Many Requests)
* response is returned.
*/
const rateLimit = RateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 100 requests per windowMs on the root path
max: 5,
message: "Too many requests, please try again later.",
statusCode: 429, // HTTP status code for "Too Many Requests"
statusCode: 429,
});
module.exports = {
rateLimite
rateLimit
}

View File

@@ -1,21 +1,43 @@
const path = require("path");
function isValidUrl(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
/**
* Validates whether the provided string is a well-formed URL.
*
* The validation relies on the native URL constructor and
* returns `true` only if the value can be successfully parsed.
*
* @param {string} url - URL to validate.
* @returns {boolean} True if the URL is valid, otherwise false.
*/
function isValidUrl(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
}
/**
* Validates whether a folder path is considered safe for
* file operations.
*
* Security checks:
* - Rejects empty or very short paths.
* - Resolves the absolute path before validation.
* - Prevents access to sensitive Windows system directories.
*
* @param {string} folder - Folder path to validate.
* @returns {boolean} True if the path is considered safe, otherwise false.
*/
function isSafePath(folder) {
if (!folder || folder.length < 3) return false;
const unsafe = ["System32", "\\Windows"];
const resolved = path.resolve(folder);
return !unsafe.some(u => resolved.includes(u));
}
module.exports = { isValidUrl, isSafePath };
module.exports = { isValidUrl, isSafePath };

View File

@@ -20,7 +20,15 @@ const logFormat = format.combine(
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
);
// Logger configuration
/**
* Logger Instance with differents types
* - info
* - error
* - warn
*
* You need to specify the text that need to be displayed
* @param string
*/
const logger = createLogger({
level: "info",
format: logFormat,
@@ -40,11 +48,17 @@ const logger = createLogger({
],
});
/**
* Start Log Session
*/
function logSessionStart() {
logger.info(`--- Starting session: ${new Date().toISOString()} ---`);
logger.info(`Application Version: ${config.version}`)
}
/**
* Stop Log Session
*/
function logSessionEnd() {
logger.info(`--- Ending session: ${new Date().toISOString()} ---`);
}

View File

@@ -2,7 +2,7 @@ const express = require("express");
const path = require("path");
const { logger, logSessionEnd } = require("./logger");
const config = require("../config");
const { rateLimite } = require("./helpers/rateLimit.helpers");
const { rateLimit } = require("./helpers/rateLimit.helpers");
const app = express();
@@ -14,10 +14,18 @@ app.use(express.static(path.join(__dirname, "../public")));
// Routes
app.use("/download", require("./routes/download.route"));
app.use("/info", require("./routes/info.route"));
app.get("/", rateLimite, (req, res) => {
app.get("/", rateLimit, (req, res) => {
res.sendFile(path.join(__dirname, "../public/index.html"));
});
/**
* Initializes and starts the Express HTTP server.
*
* - Binds the server to `config.applicationPort`
* - Resolves with the HTTP server instance on successful startup
* - Rejects on server binding or runtime errors
* - Registers SIGINT/SIGTERM handlers for graceful shutdown
*/
async function startServer() {
return new Promise((resolve, reject) => {
const server = app.listen(config.applicationPort, () => {
@@ -30,6 +38,12 @@ async function startServer() {
reject(err);
});
/**
* Clean exit function
* - Stop Log
* - Stop Express Server
* - Stop Electron App
*/
const gracefulExit = () => {
logSessionEnd();
server.close(() => {

View File

@@ -8,13 +8,45 @@ const path = require("path");
const { isSafePath } = require("../helpers/validation.helpers");
const { configFeatures } = require("../../config.js");
/**
* Reference to the currently running yt-dlp process.
*
* Used to support download cancellation and lifecycle
* management.
*
* @type {import("child_process").ChildProcess|null}
*/
let currentDownloadProcess = null;
// Détecte si une URL est une playlist
/**
* Determines whether a URL targets a playlist.
*
* The detection is based on the presence of the YouTube
* playlist query parameter (`list`).
*
* @param {string} url - URL to inspect.
* @returns {boolean} True if the URL appears to be a playlist.
*/
function isPlaylistUrl(url) {
return url.includes("?list=") || url.includes("&list=");
}
/**
* Creates a dedicated download folder for a playlist.
*
* The playlist title is sanitized to ensure it can be safely
* used as a filesystem path. If the target folder already
* exists, an incremented suffix is appended until a unique
* folder name is found.
*
* If folder creation fails, a fallback folder named
* "Untitled Playlist X" is generated.
*
* @param {string} basePath - Parent download directory.
* @param {string} playlistTitle - Playlist title returned by yt-dlp.
* @returns {string} Absolute path of the created playlist folder.
* @throws {Error} If no valid folder can be created.
*/
function createPlaylistFolder(basePath, playlistTitle) {
const sanitizedTitle = playlistTitle
.replace(/[<>:"|?*]/g, '')
@@ -74,6 +106,32 @@ function createPlaylistFolder(basePath, playlistTitle) {
}
}
/**
* Starts a yt-dlp download process and monitors its progress.
*
* Responsibilities:
* - Validates and prepares the output directory
* - Creates playlist folders when enabled
* - Builds yt-dlp command arguments
* - Spawns the yt-dlp process
* - Parses progress, speed and stage information
* - Notifies listeners about download events
* - Handles download completion and failures
*
* @param {Object} options - Download options.
* @param {string} options.url - Video or playlist URL.
* @param {string} [options.outputFolder] - Destination folder.
* @param {string} [options.playlistTitle] - Playlist title.
*
* @param {Function[]} listeners - Progress listeners.
* @param {Function[]} speedListeners - Download speed listeners.
* @param {Function[]} stageListeners - Processing stage listeners.
* @param {Function[]} playlistInfoListeners - Playlist progress listeners.
*
* @returns {Promise<string>}
* Resolves with the final output folder path once the
* download has completed successfully.
*/
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) {
return new Promise((resolve, reject) => {
@@ -191,6 +249,16 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
});
}
/**
* Cancels the currently running yt-dlp process.
*
* Attempts to terminate the entire process tree to ensure
* that yt-dlp and any spawned FFmpeg processes are stopped.
*
* @returns {boolean}
* True if a download process was cancelled,
* otherwise false.
*/
function cancelDownload() {
if (currentDownloadProcess) {
logger.info("Cancelling download and killing all child processes...");

View File

@@ -3,8 +3,30 @@ const { userYtDlp, denoPath } = require("../helpers/path.helpers");
const getUserBrowser = require("../helpers/getBrowser.helpers");
const { logger } = require("../logger");
/**
* Retrieves metadata for a video or playlist using yt-dlp.
*
* The command executes yt-dlp in JSON mode and returns the
* parsed response object without downloading any media.
*
* Features:
* - Supports videos and playlists
* - Imports browser cookies when available
* - Uses Deno for JavaScript extraction
* - Applies a 30-second timeout
* - Limits response size to 10 MB
*
* @param {string} url - Video or playlist URL to inspect.
* @returns {Promise<Object>} Resolves with the metadata returned by yt-dlp.
*
*/
function fetchInfo(url) {
const args = [
/**
* Base yt-dlp arguments used to retrieve metadata
* without downloading media files.
*/
const metadataArgs = [
"--js-runtimes", `deno:${denoPath}`,
"--dump-single-json",
"--ignore-errors",
@@ -16,9 +38,9 @@ function fetchInfo(url) {
return new Promise((resolve, reject) => {
execFile(userYtDlp,[...args, url],
execFile(userYtDlp,[...metadataArgs , url],
{ timeout: 30_000, // 30s, if more timeout
maxBuffer: 10 * 1024 * 1024 }, // 10MO max response (Default: 200ko)
maxBuffer: 10 * 1024 * 1024 }, // Maximum response size: 10 MB (Node.js default is much smaller)
(error, stdout, stderr) => {