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 = `

${data.title}

Thumbnail @@ -102,10 +121,13 @@ document.addEventListener("DOMContentLoaded", () => {
  • Catégories : ${categories}
  • `; + + // 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 };