Commentary + Custom PATH

This commit is contained in:
MasterAcnolo
2025-07-02 16:50:52 +02:00
parent 2a8d307bc8
commit 751f3d5c3e
11 changed files with 354 additions and 97 deletions

79
main.js
View File

@@ -16,19 +16,26 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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...");
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();
});

31
preload.js Normal file
View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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"),
});

View File

@@ -51,6 +51,11 @@
<button type="submit">Télécharger</button>
</form>
<div class="download-path">
Sera enregistré dans <span id="savePath"></span>
<button id="changePath">Modifier</button>
</div>
<div id="downloadStatus"></div>
<!--Il faut utiliser un formulaire, parce que on doit envoyer a un serveur
@@ -67,9 +72,33 @@
<div id="videoInfo" style="margin-top: 20px;"></div>
</div>
<script src="script/custompath.js"></script>
<script src="script/downloadstatus.js"></script>
<script src="script/fetchinfo.js"></script>
<script src="script/chirac.js"></script>
</body>
</html>
<!-- ----------------------------------------------------
| Freedom Loader [Lang FR|EN] |
----------------------------------------------------
| URL : [________________________] [Copier] |
| |
| [ ] Audio seulement (MP3) |
| [ ] Sous-titres |
| Qualité : [Best ▼] |
| Format : [MP4 ▼] |
| |
| Chapitres : [ ] Chapitre 1 |
| [ ] Chapitre 2 |
| |
| Nom de fichier : [__________] |
| Dossier : [C:/Downloads ▼] |
| |
| [ Télécharger ] |
| |
| État du téléchargement : [==== 50% ====] |
----------------------------------------------------
| Historique | Paramètres | Infos vidéo |
---------------------------------------------------- -->

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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");
}

View File

@@ -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;
}
});
});

View File

@@ -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 dattente pour informer lutilisateur
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 nest 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 sest produite.";
} finally {
// Réactive le bouton pour autoriser d'autres soumissions
// Réactive le bouton pour permettre dautres 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 linterface propre
setTimeout(() => {
statusDiv.textContent = "";
}, 5000);

View File

@@ -16,17 +16,25 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// 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 = `
<h3>${data.title}</h3>
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
@@ -102,10 +121,13 @@ document.addEventListener("DOMContentLoaded", () => {
<li><strong>Catégories :</strong> ${categories}</li>
</ul>
`;
// 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");

View File

@@ -18,51 +18,56 @@
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 sil nexiste 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 dune 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 dune 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 dhistorique
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 dhistorique
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 dautres modules
module.exports = {
logger,
logSessionStart,

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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}`);
}

View File

@@ -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.");
}

View File

@@ -16,88 +16,88 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const express = require("express"); // framework web
const { exec } = require("child_process"); // si besoin dexé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 lAPI et servir les fichiers
const { exec } = require("child_process"); // Permet dexé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 linstance 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 lutilisateur
// Définition du dossier par défaut où enregistrer les téléchargements
// On prend le dossier Téléchargements de lutilisateur Windows (USERPROFILE)
const downloadsPath = path.join(process.env.USERPROFILE, "Downloads");
const outputFolder = path.join(downloadsPath, "Freedom Loader Output");
// création du dossier sil nexiste pas
// Création du dossier de sortie sil nexiste 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 lutiliser 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 daccé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 dautres 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 derreur 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 dautres modules puissent la lancer
// Export de la fonction startServer pour permettre son appel depuis dautres fichiers
module.exports = { startServer };