diff --git a/main.js b/main.js
index af0e2b8..6017925 100644
--- a/main.js
+++ b/main.js
@@ -16,19 +16,26 @@
along with this program. If not, see .
*/
-const { app, BrowserWindow } = require("electron");
+const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const path = require("path");
+const os = require("os");
const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
let mainWindow;
+/*
+ Fonction principale qui crée la fenêtre principale de l'application.
+ Elle évite la création multiple et configure les dimensions et options de la fenêtre.
+ Charge l'URL locale du serveur Express et gère les erreurs éventuelles.
+*/
async function createWindow() {
- logger.info("Creation de la fenetre...");
-
+ logger.info("Création de la fenêtre...");
+
if (mainWindow) {
- logger.warn("La fenetre existe deja, pas de nouvelle creation");
+ logger.warn("La fenêtre existe déjà, pas de nouvelle création");
return;
}
+
mainWindow = new BrowserWindow({
title: "Freedom Loader",
width: 750,
@@ -38,42 +45,92 @@ async function createWindow() {
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
+ preload: path.join(__dirname, "preload.js"), // Chargement du preload script sécurisé
},
- // titleBarStyle: 'hidden',
+ // titleBarStyle: 'hidden', // Option possible pour une barre de titre personnalisée
});
try {
await mainWindow.loadURL("http://localhost:8080");
- logger.info("Fenetre chargee");
+ logger.info("Fenêtre chargée");
} catch (err) {
- logger.error("Erreur chargement fenetre:", err);
+ logger.error("Erreur chargement fenêtre:", err);
}
+ // Événement déclenché à la fermeture de la fenêtre principale
mainWindow.on("closed", () => {
- logger.info("Fenetre principale fermee");
+ logger.info("Fenêtre principale fermée");
mainWindow = null;
});
}
+/*
+ Définition du chemin par défaut pour les téléchargements.
+ Ici, on utilise le dossier 'Downloads' de l'utilisateur avec un sous-dossier spécifique à l'application.
+*/
+const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
+
+/*
+ Gestionnaire IPC qui permet au renderer de demander à l'utilisateur
+ de sélectionner un dossier via une boîte de dialogue native.
+ On retourne le chemin sélectionné ou null si annulation.
+*/
+ipcMain.handle("select-download-folder", async () => {
+ logger.info("Demande de sélection d'un dossier reçue depuis le renderer");
+ try {
+ const result = await dialog.showOpenDialog({
+ properties: ["openDirectory"]
+ });
+ if (!result.canceled && result.filePaths.length > 0) {
+ logger.info(`Dossier sélectionné : ${result.filePaths[0]}`);
+ return result.filePaths[0];
+ }
+ return null;
+ } catch (err) {
+ logger.error(`Erreur lors de la sélection de dossier : ${err.message}`);
+ return null;
+ }
+});
+
+/*
+ Gestionnaire IPC pour exposer au renderer le chemin par défaut de téléchargement
+ afin qu'il puisse l'afficher ou l'utiliser comme valeur initiale.
+*/
+ipcMain.handle("get-default-download-path", () => {
+ return defaultDownloadPath;
+});
+
+/*
+ Événement déclenché quand l'application est prête.
+ On démarre le serveur Express, puis on crée la fenêtre principale.
+ En cas d'erreur, on log et on quitte l'application proprement.
+*/
app.whenReady().then(async () => {
- logSessionStart();
- logger.info("App prete, demarrage du serveur Express...");
+ logSessionStart();
+ logger.info("App prête, démarrage du serveur Express...");
const expressServer = require("./server/server.js");
try {
await expressServer.startServer();
- logger.info("Serveur Express demarre");
+ logger.info("Serveur Express démarré");
await createWindow();
} catch (error) {
- logger.error("Erreur serveur ou fenetre :", error);
+ logger.error("Erreur serveur ou fenêtre :", error);
app.quit();
}
});
+/*
+ Quitte l'application lorsque toutes les fenêtres sont fermées,
+ sauf sous macOS où il est habituel de garder l'application active.
+*/
app.on("window-all-closed", () => {
- logger.info("Toutes fenetres fermees, quitte l'app");
+ logger.info("Toutes fenêtres fermées, quitte l'app");
if (process.platform !== "darwin") app.quit();
});
+/*
+ Avant de quitter l'application, on log la fin de session pour traçabilité.
+*/
app.on("before-quit", () => {
- logSessionEnd();
+ logSessionEnd();
});
diff --git a/preload.js b/preload.js
new file mode 100644
index 0000000..6a2f152
--- /dev/null
+++ b/preload.js
@@ -0,0 +1,31 @@
+/*
+ This file is part of Freedom Loader.
+
+ Copyright (C) 2025 MasterAcnolo
+
+ Freedom Loader is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License.
+
+ Freedom Loader is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+const { contextBridge, ipcRenderer } = require("electron");
+
+/*
+ Expose dans le contexte global (window.electronAPI) deux méthodes sécurisées
+ pour que le renderer puisse interagir avec le main process via IPC.
+
+ - getDefaultDownloadPath : récupère le chemin de téléchargement par défaut.
+ - selectDownloadFolder : ouvre la boîte de dialogue pour choisir un dossier.
+*/
+contextBridge.exposeInMainWorld("electronAPI", {
+ getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
+ selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
+});
diff --git a/public/index.html b/public/index.html
index b535324..2a1f95d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -51,6 +51,11 @@
+
+ Sera enregistré dans
+
+
+
\ No newline at end of file
diff --git a/public/script/chirac.js b/public/script/chirac.js
index 39bb1fd..fab95b6 100644
--- a/public/script/chirac.js
+++ b/public/script/chirac.js
@@ -1,3 +1,21 @@
+/*
+ This file is part of Freedom Loader.
+
+ Copyright (C) 2025 MasterAcnolo
+
+ Freedom Loader is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License.
+
+ Freedom Loader is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
function Chirac(){
const title = document.getElementById("title");
title.innerHTML = "Chirac Loader";
@@ -12,5 +30,4 @@ function Chirac(){
document.body.style.backgroundAttachment = "fixed";
console.log("Je serai le président de tous les français");
-
}
\ No newline at end of file
diff --git a/public/script/custompath.js b/public/script/custompath.js
new file mode 100644
index 0000000..c7b047e
--- /dev/null
+++ b/public/script/custompath.js
@@ -0,0 +1,39 @@
+/*
+ Ce script attend que le DOM soit complètement chargé pour initialiser
+ l'affichage et la gestion du chemin de sauvegarde personnalisé.
+
+ 1. Récupère le chemin de téléchargement par défaut depuis le main process via l'API exposée.
+ 2. Met à jour le texte affiché dans l'élément avec l'id "savePath".
+ 3. Crée un input caché nommé "savePath" dans le formulaire pour envoyer ce chemin lors du submit.
+ 4. Ajoute un écouteur sur le bouton "changePath" pour permettre à l'utilisateur
+ de choisir un dossier via une boîte de dialogue native.
+ 5. Met à jour l'affichage et la valeur cachée du chemin sélectionné.
+*/
+window.addEventListener("DOMContentLoaded", async () => {
+ const defaultPath = await window.electronAPI.getDefaultDownloadPath();
+ const savePathElem = document.getElementById("savePath");
+
+ if (savePathElem) {
+ savePathElem.textContent = defaultPath;
+ }
+
+ // Vérifie si l'input caché existe déjà, sinon le crée et l'ajoute au formulaire
+ let hidden = document.getElementById("savePathInput");
+ if (!hidden) {
+ hidden = document.createElement("input");
+ hidden.type = "hidden";
+ hidden.name = "savePath";
+ hidden.id = "savePathInput";
+ document.getElementById("downloadForm").appendChild(hidden);
+ }
+ hidden.value = defaultPath;
+
+ // Gestion du clic sur le bouton pour changer le dossier de téléchargement
+ document.getElementById("changePath").addEventListener("click", async () => {
+ const selectedPath = await window.electronAPI.selectDownloadFolder();
+ if (selectedPath) {
+ savePathElem.textContent = selectedPath;
+ hidden.value = selectedPath;
+ }
+ });
+});
diff --git a/public/script/downloadstatus.js b/public/script/downloadstatus.js
index 435efcf..9f30b03 100644
--- a/public/script/downloadstatus.js
+++ b/public/script/downloadstatus.js
@@ -19,53 +19,53 @@
// Récupération du formulaire de téléchargement
const form = document.getElementById("downloadForm");
-// Div où on affiche l'état de progression (en cours, erreur, succès)
+// Élément où on affichera l’état du téléchargement (en cours, erreur, succès)
const statusDiv = document.getElementById("downloadStatus");
-// Récupération du bouton de validation pour pouvoir le désactiver pendant la requête
+// Récupération du bouton de soumission pour pouvoir le désactiver pendant la requête
const button = form.querySelector("button");
-// On intercepte la soumission du formulaire
+// Écouteur d’événement pour intercepter la soumission du formulaire
form.addEventListener("submit", async (e) => {
- e.preventDefault(); // empêche le rechargement de la page par le navigateur
+ e.preventDefault(); // Empêche le rechargement automatique de la page
- // On désactive le bouton pour éviter les clics multiples
+ // Désactive le bouton pour éviter les clics multiples pendant la requête
button.disabled = true;
- // Affiche un message d'attente à l'utilisateur
+ // Affiche un message d’attente pour informer l’utilisateur
statusDiv.textContent = "Téléchargement en cours...";
- // Prépare les données du formulaire sous forme URL-encoded
+ // Prépare les données du formulaire au format URL-encoded
const formData = new FormData(form);
const params = new URLSearchParams(formData);
try {
- // Envoi de la requête POST vers /download avec les données du formulaire
+ // Envoie la requête POST vers /download avec les données du formulaire
const res = await fetch("/download", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params,
});
- // Si la réponse n'est pas un succès (code 4xx ou 5xx)
+ // Si la réponse n’est pas un succès (code HTTP 4xx ou 5xx)
if (!res.ok) {
statusDiv.textContent = "❌ Erreur pendant le téléchargement.";
return;
}
- // On récupère le texte de la réponse du serveur
+ // Récupère le texte envoyé par le serveur (message de succès ou erreur)
const text = await res.text();
- // Affiche le message de fin de téléchargement (par exemple « Téléchargement terminé »)
+ // Affiche le message retourné par le serveur
statusDiv.textContent = text;
} catch {
- // Gestion d'erreur en cas de problème réseau ou autre
+ // Gestion des erreurs réseau ou autres exceptions
statusDiv.textContent = "❌ Une erreur s’est produite.";
} finally {
- // Réactive le bouton pour autoriser d'autres soumissions
+ // Réactive le bouton pour permettre d’autres soumissions
button.disabled = false;
- // Efface le message de status après 5 secondes pour garder l'UI propre
+ // Efface le message d’état après 5 secondes pour garder l’interface propre
setTimeout(() => {
statusDiv.textContent = "";
}, 5000);
diff --git a/public/script/fetchinfo.js b/public/script/fetchinfo.js
index a60635d..4287f97 100644
--- a/public/script/fetchinfo.js
+++ b/public/script/fetchinfo.js
@@ -16,17 +16,25 @@
along with this program. If not, see .
*/
+// On attend que le DOM soit complètement chargé
document.addEventListener("DOMContentLoaded", () => {
+ // Récupération des éléments du DOM pour interaction
const urlInput = document.getElementById("UrlInput");
const infoDiv = document.getElementById("videoInfo");
+ // Stocke la dernière URL fetchée pour éviter les requêtes répétées inutiles
let lastFetchedUrl = "";
+
+ // Active les logs DEBUG pour le développement
const DEBUG = true;
+ // Écoute les changements dans l'input URL
urlInput.addEventListener("input", async () => {
const url = urlInput.value.trim();
+
if (DEBUG) console.log("[DEBUG] Input détecté :", url);
+ // Si l'URL est vide ou trop courte, on reset l'affichage
if (!url || url.length < 5) {
if (DEBUG) console.log("[DEBUG] URL vide ou trop courte, reset affichage.");
infoDiv.innerHTML = "";
@@ -35,15 +43,19 @@ document.addEventListener("DOMContentLoaded", () => {
return;
}
+ // Si l'URL est identique à la dernière requêtée, on ne refait pas la requête
if (url === lastFetchedUrl) {
if (DEBUG) console.log("[DEBUG] Même URL que précédemment, pas de fetch.");
return;
}
+ // Mémorise l'URL actuelle comme dernière URL traitée
lastFetchedUrl = url;
try {
if (DEBUG) console.log("[DEBUG] Envoi requête POST vers /info avec URL:", url);
+
+ // Envoi de la requête POST pour récupérer les infos de la vidéo
const res = await fetch("/info", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -52,6 +64,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (DEBUG) console.log("[DEBUG] Réponse status:", res.status);
+ // Si la réponse HTTP est une erreur, on affiche un message
if (!res.ok) {
console.error(`[ERROR] Réponse HTTP ${res.status}: ${res.statusText}`);
infoDiv.innerHTML = "❌ Impossible de récupérer les infos (erreur serveur).";
@@ -59,10 +72,11 @@ document.addEventListener("DOMContentLoaded", () => {
return;
}
+ // Parse la réponse JSON contenant les infos vidéo
const data = await res.json();
if (DEBUG) console.log("[DEBUG] Données reçues:", data);
- // Vérification minimaliste
+ // Vérification simple que les données attendues sont présentes
if (!data || !data.title || !data.thumbnail) {
console.warn("[WARNING] Données incomplètes", data);
infoDiv.innerHTML = "❌ Données incomplètes reçues.";
@@ -70,23 +84,28 @@ document.addEventListener("DOMContentLoaded", () => {
return;
}
+ // Formatage de la durée en minutes et secondes
const minutes = Math.floor(data.duration / 60);
const seconds = data.duration % 60;
const durationStr = `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
+ // Formatage de la date d'upload au format JJ/MM/AAAA
const date = data.upload_date || "";
const readableDate = date
? `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}`
: "Inconnue";
+ // Conversion taille approximative en Mo
const sizeMB = data.filesize_approx
? (data.filesize_approx / (1024 * 1024)).toFixed(2) + " Mo"
: "Inconnue";
+ // Gestion des catégories, affichage par défaut si absentes
const categories = data.categories
? data.categories.join(", ")
: "Non spécifiées";
+ // Construction du HTML pour afficher les informations de la vidéo
infoDiv.innerHTML = `
`;
+
+ // Affiche la div contenant les infos
infoDiv.classList.add("visible");
if (DEBUG) console.log("[DEBUG] Info affichée avec succès.");
} catch (e) {
+ // En cas d'erreur réseau ou JSON, on affiche un message d'erreur
console.error("[CRITICAL] Erreur lors de la récupération:", e);
infoDiv.innerHTML = "❌ Erreur réseau ou JSON.";
infoDiv.classList.remove("visible");
diff --git a/server/logger.js b/server/logger.js
index d8b5cb8..59f7a18 100644
--- a/server/logger.js
+++ b/server/logger.js
@@ -16,53 +16,58 @@
along with this program. If not, see .
*/
-const { createLogger, format, transports } = require("winston");
-const DailyRotateFile = require("winston-daily-rotate-file");
+const { createLogger, format, transports } = require("winston");
+const DailyRotateFile = require("winston-daily-rotate-file");
+const fs = require("fs"); // Pour vérifier/créer le dossier de logs
+const path = require("path"); // Pour gérer proprement les chemins
-const fs = require("fs"); // pour vérifier/créer le dossier de logs
-const path = require("path"); // pour gérer proprement les chemins
-
-// on définit le dossier de logs
+// Définition du dossier où seront stockés les logs
const logDir = path.join(__dirname, "../logs");
-// on le crée si inexistant
+
+// Création du dossier logs s’il n’existe pas encore
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir);
-// fonction pour générer une ligne de début de session
+// Fonction utilitaire pour générer une ligne indiquant le début d’une session de logs
function getSessionStartLine() {
const now = new Date().toISOString();
return `--- Démarrage de la session : ${now} ---`;
}
-// fonction pour générer une ligne de fin de session
+// Fonction utilitaire pour générer une ligne indiquant la fin d’une session de logs
function getSessionEndLine() {
const now = new Date().toISOString();
return `--- Fin de la session : ${now} ---`;
}
-// configuration principale de Winston
+// Configuration principale de Winston
+// - Niveau minimum d’écriture : info (enregistre info, warn, error, etc.)
+// - Formatage des logs avec timestamp lisible et format personnalisé
+// - Transports : écriture dans fichier journalier + console avec couleurs
const logger = createLogger({
- level: "info", // niveau de log minimum (info, warn, error, etc.)
+ level: "info",
format: format.combine(
- format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), // timestamp lisible
+ format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
format.printf(({ timestamp, level, message }) =>
- `${timestamp} | ${level.toUpperCase()} | ${message}` // format de ligne de log
+ `${timestamp} | ${level.toUpperCase()} | ${message}`
)
),
transports: [
+ // Rotation quotidienne des fichiers de logs
new DailyRotateFile({
- dirname: logDir, // dossier de logs
- filename: "LOGS-%DATE%.log", // nom de fichier journalier
- datePattern: "YYYY-MM-DD", // pattern de rotation
- zippedArchive: false, // true = compression des archives
- maxFiles: "14d", // garde 14 jours d’historique
+ dirname: logDir, // dossier cible pour les logs
+ filename: "LOGS-%DATE%.log", // nom du fichier par date
+ datePattern: "YYYY-MM-DD", // pattern de date dans le nom
+ zippedArchive: false, // ne pas compresser les archives
+ maxFiles: "14d", // conserve 14 jours d’historique
format: format.combine(
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
format.printf(({ timestamp, level, message }) =>
`${timestamp} | ${level.toUpperCase()} | ${message}`
)
),
- options: { flags: "a" }, // mode append
+ options: { flags: "a" }, // mode ajout à la fin (append)
}),
+ // Affichage des logs en console avec coloration et format simple
new transports.Console({
format: format.combine(
format.colorize(),
@@ -75,17 +80,17 @@ const logger = createLogger({
],
});
-// fonction d'utilitaire pour marquer le début de la session
+// Helper pour marquer clairement le début de session dans les logs
function logSessionStart() {
logger.info(getSessionStartLine());
}
-// pour la fin de la session
+// Helper pour marquer la fin de session dans les logs
function logSessionEnd() {
logger.info(getSessionEndLine());
}
-// exporte le logger et les helpers pour les autres modules
+// Export du logger principal et des helpers pour usage dans d’autres modules
module.exports = {
logger,
logSessionStart,
diff --git a/server/routes/download.js b/server/routes/download.js
index 9c9d7d8..30ad166 100644
--- a/server/routes/download.js
+++ b/server/routes/download.js
@@ -11,9 +11,6 @@
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
*/
const express = require("express");
@@ -24,40 +21,68 @@ const fs = require("fs");
const logger = require("../logger").logger;
-// Path vers le .exe de YT-DLP
+// Path vers le fichier exécutable yt-dlp (outil tiers pour le téléchargement)
const ytDlpPath = path.join(__dirname, '../../yt-dlp.exe');
router.post("/", (req, res) => {
try {
- // récupère les options envoyées depuis le frontend
+ // Récupération des options envoyées par le frontend via le formulaire
+ // On vérifie la présence de l'URL, le choix audioOnly, et la qualité demandée
const options = {
url: req.body.url,
audioOnly: req.body.audioOnly === "1",
quality: req.body.quality || "best",
};
+ // Validation simple : si l'URL est manquante, on rejette la requête
if (!options.url) {
logger.warn("Requête POST /download sans URL");
return res.status(400).send("❌ URL manquante !");
}
- const outputFolder = req.app.locals.outputFolder;
+ // Récupération du chemin de sauvegarde personnalisé envoyé depuis Electron,
+ // ou on utilise un chemin par défaut stocké dans app.locals
+ let requestedOutputFolder = req.body.savePath || req.app.locals.outputFolder;
- if (!fs.existsSync(outputFolder)) {
- fs.mkdirSync(outputFolder, { recursive: true });
- logger.info("Dossier Freedom Loader Output créé à la volée dans Téléchargements.");
+ // On normalise le chemin pour éviter des erreurs liées aux séparateurs ou chemins relatifs
+ requestedOutputFolder = path.normalize(requestedOutputFolder);
+
+ /*
+ Sécurité basique : On refuse certains chemins sensibles ou suspects
+ (ex: System32, /etc, Windows) pour éviter que l'application n'écrive dans des dossiers système.
+ On vérifie aussi la longueur minimale du chemin pour éviter des valeurs vides ou invalides.
+ */
+ if (
+ !requestedOutputFolder ||
+ requestedOutputFolder.length < 3 ||
+ requestedOutputFolder.includes("System32") ||
+ requestedOutputFolder.includes("/etc") ||
+ requestedOutputFolder.includes("\\Windows")
+ ) {
+ logger.warn(`Chemin potentiellement dangereux refusé : ${requestedOutputFolder}`);
+ return res.status(400).send("❌ Chemin de sauvegarde non autorisé.");
}
- const outputTemplate = path.join(outputFolder, "%(title)s.%(ext)s");
+ // Vérifie que le dossier de sortie existe, sinon le crée récursivement
+ if (!fs.existsSync(requestedOutputFolder)) {
+ fs.mkdirSync(requestedOutputFolder, { recursive: true });
+ logger.info(`Dossier de sortie créé : ${requestedOutputFolder}`);
+ }
- // tableau d'arguments yt-dlp
+ // Construction du template de sortie pour yt-dlp (nom de fichier basé sur le titre)
+ const outputTemplate = path.join(requestedOutputFolder, "%(title)s.%(ext)s");
+
+ // Construction du tableau d'arguments à passer à yt-dlp
+ // --no-continue pour forcer un téléchargement propre
+ // --restrict-filenames pour éviter les caractères problématiques dans les noms
const args = ["--no-continue", "--restrict-filenames"];
+ // Si l'option audioOnly est activée, on ajoute les flags pour extraction audio en mp3
if (options.audioOnly) {
args.push("--extract-audio", "--audio-format", "mp3");
}
- // gestion qualité
+ // Correspondance entre la qualité choisie et le format à demander à yt-dlp
const qualityMap = {
best: "bestvideo+bestaudio/best",
medium: "bestvideo[height<=720]+bestaudio/best[height<=720]",
@@ -67,34 +92,55 @@ router.post("/", (req, res) => {
480: "bestvideo[height<=480]+bestaudio/best[height<=480]",
};
+ // Sélection du format basé sur la qualité, par défaut 'best' si option invalide
const format = qualityMap[options.quality] || "best";
args.push("-f", format);
+ // Ajout du template de sortie et de l'URL à télécharger
args.push("-o", outputTemplate);
args.push(options.url);
+ // Log de la commande complète pour traçabilité et débogage
logger.info(`Téléchargement demandé : url=${options.url}, audioOnly=${options.audioOnly}, quality=${options.quality}`);
logger.info(`Commande yt-dlp : ${ytDlpPath} ${args.join(" ")}`);
+ // Lancement du processus yt-dlp avec les arguments définis
const child = execFile(ytDlpPath, args);
+ /*
+ Gestion des sorties standards (stdout) du processus,
+ on log chaque ligne pour suivre la progression ou informations diverses
+ */
child.stdout.on("data", (data) => {
data.toString().split("\n").forEach(line => {
if (line.trim()) logger.info(`[yt-dlp stdout] ${line.trim()}`);
});
});
+ /*
+ Gestion des erreurs (stderr) du processus,
+ on log chaque ligne d'erreur pour diagnostic
+ */
child.stderr.on("data", (data) => {
data.toString().split("\n").forEach(line => {
if (line.trim()) logger.error(`[yt-dlp stderr] ${line.trim()}`);
});
});
+ /*
+ En cas d'erreur lors du lancement du processus yt-dlp,
+ on log l'erreur et on répond au client avec un statut 500
+ */
child.on("error", (err) => {
logger.error(`Erreur lancement yt-dlp : ${err.message}`);
res.status(500).send(`❌ Erreur lors de l'exécution : ${err.message}`);
});
+ /*
+ Quand le processus se termine, on vérifie le code de sortie.
+ Code 0 = succès, on informe le client que le téléchargement est terminé.
+ Sinon, on envoie une erreur au client avec le code d'échec.
+ */
child.on("close", (code) => {
logger.info(`yt-dlp terminé avec code de sortie : ${code}`);
if (code === 0) {
@@ -105,6 +151,7 @@ router.post("/", (req, res) => {
});
} catch (err) {
+ // Capture toute autre erreur serveur non prévue, log et réponse 500
logger.error(`Erreur serveur dans /download : ${err.message}`);
res.status(500).send(`Erreur serveur : ${err.message}`);
}
diff --git a/server/routes/info.js b/server/routes/info.js
index b7739d4..a1aae33 100644
--- a/server/routes/info.js
+++ b/server/routes/info.js
@@ -21,18 +21,22 @@ const router = express.Router();
const { execFile } = require("child_process");
const path = require("path");
const fs = require("fs");
-const { logger } = require("../logger"); // on récupère ton logger Winston
+const { logger } = require("../logger"); // On récupère ton logger Winston
+// Path absolu vers l'exécutable yt-dlp
const ytDlpPath = path.join(__dirname, "../../yt-dlp.exe");
-// Check si ya YT DLP
+// Vérification que yt-dlp.exe existe bien au lancement du module
if (!fs.existsSync(ytDlpPath)) {
logger.error(`❌ yt-dlp.exe introuvable à ${ytDlpPath}`);
throw new Error(`yt-dlp.exe introuvable à ${ytDlpPath}`);
}
+// Route POST pour récupérer les métadonnées d'une vidéo via yt-dlp
router.post("/", (req, res) => {
const url = req.body.url;
+
+ // Validation basique : on refuse la requête sans URL
if (!url) {
logger.warn("Requête metadata sans URL");
return res.status(400).send("❌ URL manquante");
@@ -40,27 +44,33 @@ router.post("/", (req, res) => {
logger.info(`Requête metadata reçue pour ${url}`);
+ // Exécution de yt-dlp avec l'option --dump-json pour récupérer les infos vidéo
execFile(
ytDlpPath,
["--dump-json", url],
- { timeout: 10_000 }, // 10s de timeout
+ { timeout: 10_000 }, // Timeout fixé à 10 secondes pour éviter blocage
(error, stdout, stderr) => {
if (error) {
+ // En cas d'erreur, on log l'erreur et le stderr, puis on renvoie un code 500
logger.error(`Erreur exécution yt-dlp: ${error.message}`);
logger.debug(`stderr: ${stderr}`);
return res.status(500).send("❌ Impossible de récupérer les infos.");
}
try {
- // découpe multi-JSON propre
+ // yt-dlp peut retourner plusieurs JSON séparés par des sauts de ligne
+ // On split et parse chacun proprement
const infos = stdout
.trim()
.split("\n")
.map(line => JSON.parse(line));
logger.info(`Infos récupérées pour ${url} (${infos.length} élément(s))`);
+
+ // Si on a un seul élément, on renvoie directement l'objet JSON, sinon un tableau
res.json(infos.length === 1 ? infos[0] : infos);
} catch (e) {
+ // Si parsing JSON échoue, on log et renvoie une erreur 500
logger.error(`Erreur parsing JSON: ${e.message}`);
return res.status(500).send("❌ JSON illisible.");
}
diff --git a/server/server.js b/server/server.js
index ba05351..54ba18f 100644
--- a/server/server.js
+++ b/server/server.js
@@ -16,88 +16,88 @@
along with this program. If not, see .
*/
-const express = require("express"); // framework web
-const { exec } = require("child_process"); // si besoin d’exécuter des commandes (pas utilisé ici directement)
-const fs = require("fs"); // accès au système de fichiers
-const path = require("path"); // gestion de chemins
+const express = require("express"); // Framework web pour créer l’API et servir les fichiers
+const { exec } = require("child_process"); // Permet d’exécuter des commandes systèmes (pas utilisé directement ici)
+const fs = require("fs"); // Module système pour gérer fichiers et dossiers
+const path = require("path"); // Gestion propre des chemins de fichiers et dossiers
const debug = require("debug")("freedom-loader:server");
-// petit debugger coloré en console
+// Module de debug coloré en console, pratique pour dev
const { logger, logSessionStart, logSessionEnd } = require("./logger");
-// récupère notre logger Winston + helpers de session
+// Import du logger Winston et helpers pour marquer début/fin de session
-const app = express(); // instancie l'application Express
+const app = express(); // Création de l’instance Express, notre serveur web
-// logSessionStart(); // désactivé, mais pourrait logguer le début de session
-
-// on définit le dossier de téléchargement dans le dossier Téléchargements de l’utilisateur
+// Définition du dossier par défaut où enregistrer les téléchargements
+// On prend le dossier Téléchargements de l’utilisateur Windows (USERPROFILE)
const downloadsPath = path.join(process.env.USERPROFILE, "Downloads");
const outputFolder = path.join(downloadsPath, "Freedom Loader Output");
-// création du dossier s’il n’existe pas
+// Création du dossier de sortie s’il n’existe pas déjà
if (!fs.existsSync(outputFolder)) {
try {
- fs.mkdirSync(outputFolder, { recursive: true });
+ fs.mkdirSync(outputFolder, { recursive: true }); // création récursive au cas où
logger.info("Dossier Freedom Loader Output cree dans Telechargements.");
} catch (err) {
logger.error("Impossible de creer le dossier :", err);
- process.exit(1); // arrêt du programme en cas d’échec
+ process.exit(1); // Arrêt du programme si dossier non créé (critique)
}
} else {
logger.info("Dossier Freedom Loader Output deja existant.");
}
-// on rend ce dossier dispo via app.locals
+// On rend ce dossier accessible globalement via app.locals pour l’utiliser dans les routes
app.locals.outputFolder = outputFolder;
-// configuration pour parser les requêtes POST en urlencoded
+// Middleware pour parser le corps des requêtes POST en application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }));
-// sert les fichiers statiques (frontend)
+// Définition du dossier contenant les fichiers statiques (frontend)
+// Permet d’accéder au HTML, CSS, JS côté client
const staticPath = path.join(__dirname, "../public");
debug("Serveur statique sur", staticPath);
app.use(express.static(staticPath));
-// enregistre les routes API
+// Import et enregistrement des routes API
+// Ces routes gèrent les requêtes pour les infos vidéo et téléchargement
const infoRoute = require("./routes/info");
const downloadRoute = require("./routes/download");
debug("Routes /download et /info installees");
app.use("/download", downloadRoute);
app.use("/info", infoRoute);
-// route GET / de base
+// Route GET / qui sert la page principale index.html
app.get("/", (req, res) => {
debug("Requete GET / servie");
res.sendFile(path.join(__dirname, "../public/index.html"));
});
-// fonction pour démarrer le serveur (Promise pour pouvoir await ailleurs)
+// Fonction pour démarrer le serveur Express
+// Retourne une Promise pour pouvoir await le démarrage dans d’autres modules
function startServer() {
return new Promise((resolve, reject) => {
logger.info("Demarrage du serveur Express...");
const serverInstance = app.listen(8080, () => {
logger.info("Serveur Express pret sur http://localhost:8080");
- resolve(serverInstance); // succès
+ resolve(serverInstance); // Serveur prêt, on résout la promesse
});
+ // Gestion des erreurs serveur lors du démarrage
serverInstance.on("error", (err) => {
logger.error("Erreur serveur Express :", err);
- reject(err);
+ reject(err); // Rejet de la promesse en cas d’erreur critique
});
});
}
-// gestion de la fermeture propre du process
-// process.on("exit", () => {
-// logSessionEnd();
-// });
-process.on("SIGINT", () => { // Ctrl+C
+// Gestion propre de la fermeture du process pour logger la fin de session
+process.on("SIGINT", () => { // Capture Ctrl+C (interruption)
logSessionEnd();
process.exit();
});
-process.on("SIGTERM", () => { // kill
+process.on("SIGTERM", () => { // Capture kill ou arrêt du process
logSessionEnd();
process.exit();
});
-// on exporte la fonction startServer pour que d’autres modules puissent la lancer
+// Export de la fonction startServer pour permettre son appel depuis d’autres fichiers
module.exports = { startServer };