mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-07-29 18:25:47 +02:00
Commentary + Custom PATH
This commit is contained in:
@@ -16,53 +16,58 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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,
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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 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 };
|
||||
|
||||
Reference in New Issue
Block a user