mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-07-29 18:25:47 +02:00
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:
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user