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:
79
main.js
79
main.js
@@ -16,19 +16,26 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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 path = require("path");
|
||||||
|
const os = require("os");
|
||||||
const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
|
const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
|
||||||
|
|
||||||
let mainWindow;
|
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() {
|
async function createWindow() {
|
||||||
logger.info("Creation de la fenetre...");
|
logger.info("Création de la fenêtre...");
|
||||||
|
|
||||||
if (mainWindow) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
title: "Freedom Loader",
|
title: "Freedom Loader",
|
||||||
width: 750,
|
width: 750,
|
||||||
@@ -38,42 +45,92 @@ async function createWindow() {
|
|||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
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 {
|
try {
|
||||||
await mainWindow.loadURL("http://localhost:8080");
|
await mainWindow.loadURL("http://localhost:8080");
|
||||||
logger.info("Fenetre chargee");
|
logger.info("Fenêtre chargée");
|
||||||
} catch (err) {
|
} 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", () => {
|
mainWindow.on("closed", () => {
|
||||||
logger.info("Fenetre principale fermee");
|
logger.info("Fenêtre principale fermée");
|
||||||
mainWindow = null;
|
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 () => {
|
app.whenReady().then(async () => {
|
||||||
logSessionStart();
|
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");
|
const expressServer = require("./server/server.js");
|
||||||
try {
|
try {
|
||||||
await expressServer.startServer();
|
await expressServer.startServer();
|
||||||
logger.info("Serveur Express demarre");
|
logger.info("Serveur Express démarré");
|
||||||
await createWindow();
|
await createWindow();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Erreur serveur ou fenetre :", error);
|
logger.error("Erreur serveur ou fenêtre :", error);
|
||||||
app.quit();
|
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", () => {
|
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();
|
if (process.platform !== "darwin") app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Avant de quitter l'application, on log la fin de session pour traçabilité.
|
||||||
|
*/
|
||||||
app.on("before-quit", () => {
|
app.on("before-quit", () => {
|
||||||
logSessionEnd();
|
logSessionEnd();
|
||||||
});
|
});
|
||||||
|
|||||||
31
preload.js
Normal file
31
preload.js
Normal 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"),
|
||||||
|
});
|
||||||
@@ -51,6 +51,11 @@
|
|||||||
<button type="submit">Télécharger</button>
|
<button type="submit">Télécharger</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="download-path">
|
||||||
|
Sera enregistré dans <span id="savePath"></span>
|
||||||
|
<button id="changePath">Modifier</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="downloadStatus"></div>
|
<div id="downloadStatus"></div>
|
||||||
|
|
||||||
<!--Il faut utiliser un formulaire, parce que on doit envoyer a un serveur
|
<!--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 id="videoInfo" style="margin-top: 20px;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="script/custompath.js"></script>
|
||||||
<script src="script/downloadstatus.js"></script>
|
<script src="script/downloadstatus.js"></script>
|
||||||
<script src="script/fetchinfo.js"></script>
|
<script src="script/fetchinfo.js"></script>
|
||||||
<script src="script/chirac.js"></script>
|
<script src="script/chirac.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 |
|
||||||
|
---------------------------------------------------- -->
|
||||||
@@ -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(){
|
function Chirac(){
|
||||||
const title = document.getElementById("title");
|
const title = document.getElementById("title");
|
||||||
title.innerHTML = "Chirac Loader";
|
title.innerHTML = "Chirac Loader";
|
||||||
@@ -12,5 +30,4 @@ function Chirac(){
|
|||||||
document.body.style.backgroundAttachment = "fixed";
|
document.body.style.backgroundAttachment = "fixed";
|
||||||
|
|
||||||
console.log("Je serai le président de tous les français");
|
console.log("Je serai le président de tous les français");
|
||||||
|
|
||||||
}
|
}
|
||||||
39
public/script/custompath.js
Normal file
39
public/script/custompath.js
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,53 +19,53 @@
|
|||||||
// Récupération du formulaire de téléchargement
|
// Récupération du formulaire de téléchargement
|
||||||
const form = document.getElementById("downloadForm");
|
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");
|
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");
|
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) => {
|
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;
|
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...";
|
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 formData = new FormData(form);
|
||||||
const params = new URLSearchParams(formData);
|
const params = new URLSearchParams(formData);
|
||||||
|
|
||||||
try {
|
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", {
|
const res = await fetch("/download", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
body: params,
|
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) {
|
if (!res.ok) {
|
||||||
statusDiv.textContent = "❌ Erreur pendant le téléchargement.";
|
statusDiv.textContent = "❌ Erreur pendant le téléchargement.";
|
||||||
return;
|
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();
|
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;
|
statusDiv.textContent = text;
|
||||||
} catch {
|
} 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.";
|
statusDiv.textContent = "❌ Une erreur s’est produite.";
|
||||||
} finally {
|
} finally {
|
||||||
// Réactive le bouton pour autoriser d'autres soumissions
|
// Réactive le bouton pour permettre d’autres soumissions
|
||||||
button.disabled = false;
|
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(() => {
|
setTimeout(() => {
|
||||||
statusDiv.textContent = "";
|
statusDiv.textContent = "";
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|||||||
@@ -16,17 +16,25 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// On attend que le DOM soit complètement chargé
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
// Récupération des éléments du DOM pour interaction
|
||||||
const urlInput = document.getElementById("UrlInput");
|
const urlInput = document.getElementById("UrlInput");
|
||||||
const infoDiv = document.getElementById("videoInfo");
|
const infoDiv = document.getElementById("videoInfo");
|
||||||
|
|
||||||
|
// Stocke la dernière URL fetchée pour éviter les requêtes répétées inutiles
|
||||||
let lastFetchedUrl = "";
|
let lastFetchedUrl = "";
|
||||||
|
|
||||||
|
// Active les logs DEBUG pour le développement
|
||||||
const DEBUG = true;
|
const DEBUG = true;
|
||||||
|
|
||||||
|
// Écoute les changements dans l'input URL
|
||||||
urlInput.addEventListener("input", async () => {
|
urlInput.addEventListener("input", async () => {
|
||||||
const url = urlInput.value.trim();
|
const url = urlInput.value.trim();
|
||||||
|
|
||||||
if (DEBUG) console.log("[DEBUG] Input détecté :", url);
|
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 (!url || url.length < 5) {
|
||||||
if (DEBUG) console.log("[DEBUG] URL vide ou trop courte, reset affichage.");
|
if (DEBUG) console.log("[DEBUG] URL vide ou trop courte, reset affichage.");
|
||||||
infoDiv.innerHTML = "";
|
infoDiv.innerHTML = "";
|
||||||
@@ -35,15 +43,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Si l'URL est identique à la dernière requêtée, on ne refait pas la requête
|
||||||
if (url === lastFetchedUrl) {
|
if (url === lastFetchedUrl) {
|
||||||
if (DEBUG) console.log("[DEBUG] Même URL que précédemment, pas de fetch.");
|
if (DEBUG) console.log("[DEBUG] Même URL que précédemment, pas de fetch.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mémorise l'URL actuelle comme dernière URL traitée
|
||||||
lastFetchedUrl = url;
|
lastFetchedUrl = url;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (DEBUG) console.log("[DEBUG] Envoi requête POST vers /info avec URL:", url);
|
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", {
|
const res = await fetch("/info", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
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);
|
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) {
|
if (!res.ok) {
|
||||||
console.error(`[ERROR] Réponse HTTP ${res.status}: ${res.statusText}`);
|
console.error(`[ERROR] Réponse HTTP ${res.status}: ${res.statusText}`);
|
||||||
infoDiv.innerHTML = "❌ Impossible de récupérer les infos (erreur serveur).";
|
infoDiv.innerHTML = "❌ Impossible de récupérer les infos (erreur serveur).";
|
||||||
@@ -59,10 +72,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse la réponse JSON contenant les infos vidéo
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (DEBUG) console.log("[DEBUG] Données reçues:", data);
|
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) {
|
if (!data || !data.title || !data.thumbnail) {
|
||||||
console.warn("[WARNING] Données incomplètes", data);
|
console.warn("[WARNING] Données incomplètes", data);
|
||||||
infoDiv.innerHTML = "❌ Données incomplètes reçues.";
|
infoDiv.innerHTML = "❌ Données incomplètes reçues.";
|
||||||
@@ -70,23 +84,28 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Formatage de la durée en minutes et secondes
|
||||||
const minutes = Math.floor(data.duration / 60);
|
const minutes = Math.floor(data.duration / 60);
|
||||||
const seconds = data.duration % 60;
|
const seconds = data.duration % 60;
|
||||||
const durationStr = `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
|
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 date = data.upload_date || "";
|
||||||
const readableDate = date
|
const readableDate = date
|
||||||
? `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}`
|
? `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}`
|
||||||
: "Inconnue";
|
: "Inconnue";
|
||||||
|
|
||||||
|
// Conversion taille approximative en Mo
|
||||||
const sizeMB = data.filesize_approx
|
const sizeMB = data.filesize_approx
|
||||||
? (data.filesize_approx / (1024 * 1024)).toFixed(2) + " Mo"
|
? (data.filesize_approx / (1024 * 1024)).toFixed(2) + " Mo"
|
||||||
: "Inconnue";
|
: "Inconnue";
|
||||||
|
|
||||||
|
// Gestion des catégories, affichage par défaut si absentes
|
||||||
const categories = data.categories
|
const categories = data.categories
|
||||||
? data.categories.join(", ")
|
? data.categories.join(", ")
|
||||||
: "Non spécifiées";
|
: "Non spécifiées";
|
||||||
|
|
||||||
|
// Construction du HTML pour afficher les informations de la vidéo
|
||||||
infoDiv.innerHTML = `
|
infoDiv.innerHTML = `
|
||||||
<h3>${data.title}</h3>
|
<h3>${data.title}</h3>
|
||||||
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
||||||
@@ -102,10 +121,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
<li><strong>Catégories :</strong> ${categories}</li>
|
<li><strong>Catégories :</strong> ${categories}</li>
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Affiche la div contenant les infos
|
||||||
infoDiv.classList.add("visible");
|
infoDiv.classList.add("visible");
|
||||||
|
|
||||||
if (DEBUG) console.log("[DEBUG] Info affichée avec succès.");
|
if (DEBUG) console.log("[DEBUG] Info affichée avec succès.");
|
||||||
} catch (e) {
|
} 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);
|
console.error("[CRITICAL] Erreur lors de la récupération:", e);
|
||||||
infoDiv.innerHTML = "❌ Erreur réseau ou JSON.";
|
infoDiv.innerHTML = "❌ Erreur réseau ou JSON.";
|
||||||
infoDiv.classList.remove("visible");
|
infoDiv.classList.remove("visible");
|
||||||
|
|||||||
@@ -18,51 +18,56 @@
|
|||||||
|
|
||||||
const { createLogger, format, transports } = require("winston");
|
const { createLogger, format, transports } = require("winston");
|
||||||
const DailyRotateFile = require("winston-daily-rotate-file");
|
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
|
// Définition du dossier où seront stockés les logs
|
||||||
const path = require("path"); // pour gérer proprement les chemins
|
|
||||||
|
|
||||||
// on définit le dossier de logs
|
|
||||||
const logDir = path.join(__dirname, "../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);
|
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() {
|
function getSessionStartLine() {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
return `--- Démarrage de la session : ${now} ---`;
|
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() {
|
function getSessionEndLine() {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
return `--- Fin de la session : ${now} ---`;
|
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({
|
const logger = createLogger({
|
||||||
level: "info", // niveau de log minimum (info, warn, error, etc.)
|
level: "info",
|
||||||
format: format.combine(
|
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 }) =>
|
format.printf(({ timestamp, level, message }) =>
|
||||||
`${timestamp} | ${level.toUpperCase()} | ${message}` // format de ligne de log
|
`${timestamp} | ${level.toUpperCase()} | ${message}`
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
transports: [
|
transports: [
|
||||||
|
// Rotation quotidienne des fichiers de logs
|
||||||
new DailyRotateFile({
|
new DailyRotateFile({
|
||||||
dirname: logDir, // dossier de logs
|
dirname: logDir, // dossier cible pour les logs
|
||||||
filename: "LOGS-%DATE%.log", // nom de fichier journalier
|
filename: "LOGS-%DATE%.log", // nom du fichier par date
|
||||||
datePattern: "YYYY-MM-DD", // pattern de rotation
|
datePattern: "YYYY-MM-DD", // pattern de date dans le nom
|
||||||
zippedArchive: false, // true = compression des archives
|
zippedArchive: false, // ne pas compresser les archives
|
||||||
maxFiles: "14d", // garde 14 jours d’historique
|
maxFiles: "14d", // conserve 14 jours d’historique
|
||||||
format: format.combine(
|
format: format.combine(
|
||||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
format.printf(({ timestamp, level, message }) =>
|
format.printf(({ timestamp, level, message }) =>
|
||||||
`${timestamp} | ${level.toUpperCase()} | ${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({
|
new transports.Console({
|
||||||
format: format.combine(
|
format: format.combine(
|
||||||
format.colorize(),
|
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() {
|
function logSessionStart() {
|
||||||
logger.info(getSessionStartLine());
|
logger.info(getSessionStartLine());
|
||||||
}
|
}
|
||||||
|
|
||||||
// pour la fin de la session
|
// Helper pour marquer la fin de session dans les logs
|
||||||
function logSessionEnd() {
|
function logSessionEnd() {
|
||||||
logger.info(getSessionEndLine());
|
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 = {
|
module.exports = {
|
||||||
logger,
|
logger,
|
||||||
logSessionStart,
|
logSessionStart,
|
||||||
|
|||||||
@@ -11,9 +11,6 @@
|
|||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
See the GNU General Public License for more details.
|
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");
|
const express = require("express");
|
||||||
@@ -24,40 +21,68 @@ const fs = require("fs");
|
|||||||
|
|
||||||
const logger = require("../logger").logger;
|
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');
|
const ytDlpPath = path.join(__dirname, '../../yt-dlp.exe');
|
||||||
|
|
||||||
router.post("/", (req, res) => {
|
router.post("/", (req, res) => {
|
||||||
try {
|
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 = {
|
const options = {
|
||||||
url: req.body.url,
|
url: req.body.url,
|
||||||
audioOnly: req.body.audioOnly === "1",
|
audioOnly: req.body.audioOnly === "1",
|
||||||
quality: req.body.quality || "best",
|
quality: req.body.quality || "best",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Validation simple : si l'URL est manquante, on rejette la requête
|
||||||
if (!options.url) {
|
if (!options.url) {
|
||||||
logger.warn("Requête POST /download sans URL");
|
logger.warn("Requête POST /download sans URL");
|
||||||
return res.status(400).send("❌ URL manquante !");
|
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)) {
|
// On normalise le chemin pour éviter des erreurs liées aux séparateurs ou chemins relatifs
|
||||||
fs.mkdirSync(outputFolder, { recursive: true });
|
requestedOutputFolder = path.normalize(requestedOutputFolder);
|
||||||
logger.info("Dossier Freedom Loader Output créé à la volée dans Téléchargements.");
|
|
||||||
|
/*
|
||||||
|
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"];
|
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) {
|
if (options.audioOnly) {
|
||||||
args.push("--extract-audio", "--audio-format", "mp3");
|
args.push("--extract-audio", "--audio-format", "mp3");
|
||||||
}
|
}
|
||||||
|
|
||||||
// gestion qualité
|
// Correspondance entre la qualité choisie et le format à demander à yt-dlp
|
||||||
const qualityMap = {
|
const qualityMap = {
|
||||||
best: "bestvideo+bestaudio/best",
|
best: "bestvideo+bestaudio/best",
|
||||||
medium: "bestvideo[height<=720]+bestaudio/best[height<=720]",
|
medium: "bestvideo[height<=720]+bestaudio/best[height<=720]",
|
||||||
@@ -67,34 +92,55 @@ router.post("/", (req, res) => {
|
|||||||
480: "bestvideo[height<=480]+bestaudio/best[height<=480]",
|
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";
|
const format = qualityMap[options.quality] || "best";
|
||||||
args.push("-f", format);
|
args.push("-f", format);
|
||||||
|
|
||||||
|
// Ajout du template de sortie et de l'URL à télécharger
|
||||||
args.push("-o", outputTemplate);
|
args.push("-o", outputTemplate);
|
||||||
args.push(options.url);
|
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(`Téléchargement demandé : url=${options.url}, audioOnly=${options.audioOnly}, quality=${options.quality}`);
|
||||||
logger.info(`Commande yt-dlp : ${ytDlpPath} ${args.join(" ")}`);
|
logger.info(`Commande yt-dlp : ${ytDlpPath} ${args.join(" ")}`);
|
||||||
|
|
||||||
|
// Lancement du processus yt-dlp avec les arguments définis
|
||||||
const child = execFile(ytDlpPath, args);
|
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) => {
|
child.stdout.on("data", (data) => {
|
||||||
data.toString().split("\n").forEach(line => {
|
data.toString().split("\n").forEach(line => {
|
||||||
if (line.trim()) logger.info(`[yt-dlp stdout] ${line.trim()}`);
|
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) => {
|
child.stderr.on("data", (data) => {
|
||||||
data.toString().split("\n").forEach(line => {
|
data.toString().split("\n").forEach(line => {
|
||||||
if (line.trim()) logger.error(`[yt-dlp stderr] ${line.trim()}`);
|
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) => {
|
child.on("error", (err) => {
|
||||||
logger.error(`Erreur lancement yt-dlp : ${err.message}`);
|
logger.error(`Erreur lancement yt-dlp : ${err.message}`);
|
||||||
res.status(500).send(`❌ Erreur lors de l'exécution : ${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) => {
|
child.on("close", (code) => {
|
||||||
logger.info(`yt-dlp terminé avec code de sortie : ${code}`);
|
logger.info(`yt-dlp terminé avec code de sortie : ${code}`);
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
@@ -105,6 +151,7 @@ router.post("/", (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Capture toute autre erreur serveur non prévue, log et réponse 500
|
||||||
logger.error(`Erreur serveur dans /download : ${err.message}`);
|
logger.error(`Erreur serveur dans /download : ${err.message}`);
|
||||||
res.status(500).send(`Erreur serveur : ${err.message}`);
|
res.status(500).send(`Erreur serveur : ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,18 +21,22 @@ const router = express.Router();
|
|||||||
const { execFile } = require("child_process");
|
const { execFile } = require("child_process");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
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");
|
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)) {
|
if (!fs.existsSync(ytDlpPath)) {
|
||||||
logger.error(`❌ yt-dlp.exe introuvable à ${ytDlpPath}`);
|
logger.error(`❌ yt-dlp.exe introuvable à ${ytDlpPath}`);
|
||||||
throw new 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) => {
|
router.post("/", (req, res) => {
|
||||||
const url = req.body.url;
|
const url = req.body.url;
|
||||||
|
|
||||||
|
// Validation basique : on refuse la requête sans URL
|
||||||
if (!url) {
|
if (!url) {
|
||||||
logger.warn("Requête metadata sans URL");
|
logger.warn("Requête metadata sans URL");
|
||||||
return res.status(400).send("❌ URL manquante");
|
return res.status(400).send("❌ URL manquante");
|
||||||
@@ -40,27 +44,33 @@ router.post("/", (req, res) => {
|
|||||||
|
|
||||||
logger.info(`Requête metadata reçue pour ${url}`);
|
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(
|
execFile(
|
||||||
ytDlpPath,
|
ytDlpPath,
|
||||||
["--dump-json", url],
|
["--dump-json", url],
|
||||||
{ timeout: 10_000 }, // 10s de timeout
|
{ timeout: 10_000 }, // Timeout fixé à 10 secondes pour éviter blocage
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
if (error) {
|
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.error(`Erreur exécution yt-dlp: ${error.message}`);
|
||||||
logger.debug(`stderr: ${stderr}`);
|
logger.debug(`stderr: ${stderr}`);
|
||||||
return res.status(500).send("❌ Impossible de récupérer les infos.");
|
return res.status(500).send("❌ Impossible de récupérer les infos.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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
|
const infos = stdout
|
||||||
.trim()
|
.trim()
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map(line => JSON.parse(line));
|
.map(line => JSON.parse(line));
|
||||||
|
|
||||||
logger.info(`Infos récupérées pour ${url} (${infos.length} élément(s))`);
|
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);
|
res.json(infos.length === 1 ? infos[0] : infos);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// Si parsing JSON échoue, on log et renvoie une erreur 500
|
||||||
logger.error(`Erreur parsing JSON: ${e.message}`);
|
logger.error(`Erreur parsing JSON: ${e.message}`);
|
||||||
return res.status(500).send("❌ JSON illisible.");
|
return res.status(500).send("❌ JSON illisible.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,88 +16,88 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const express = require("express"); // framework web
|
const express = require("express"); // Framework web pour créer l’API et servir les fichiers
|
||||||
const { exec } = require("child_process"); // si besoin d’exécuter des commandes (pas utilisé ici directement)
|
const { exec } = require("child_process"); // Permet d’exécuter des commandes systèmes (pas utilisé directement ici)
|
||||||
const fs = require("fs"); // accès au système de fichiers
|
const fs = require("fs"); // Module système pour gérer fichiers et dossiers
|
||||||
const path = require("path"); // gestion de chemins
|
const path = require("path"); // Gestion propre des chemins de fichiers et dossiers
|
||||||
const debug = require("debug")("freedom-loader:server");
|
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");
|
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
|
// 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)
|
||||||
// on définit le dossier de téléchargement dans le dossier Téléchargements de l’utilisateur
|
|
||||||
const downloadsPath = path.join(process.env.USERPROFILE, "Downloads");
|
const downloadsPath = path.join(process.env.USERPROFILE, "Downloads");
|
||||||
const outputFolder = path.join(downloadsPath, "Freedom Loader Output");
|
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)) {
|
if (!fs.existsSync(outputFolder)) {
|
||||||
try {
|
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.");
|
logger.info("Dossier Freedom Loader Output cree dans Telechargements.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Impossible de creer le dossier :", 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 {
|
} else {
|
||||||
logger.info("Dossier Freedom Loader Output deja existant.");
|
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;
|
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 }));
|
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");
|
const staticPath = path.join(__dirname, "../public");
|
||||||
debug("Serveur statique sur", staticPath);
|
debug("Serveur statique sur", staticPath);
|
||||||
app.use(express.static(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 infoRoute = require("./routes/info");
|
||||||
const downloadRoute = require("./routes/download");
|
const downloadRoute = require("./routes/download");
|
||||||
debug("Routes /download et /info installees");
|
debug("Routes /download et /info installees");
|
||||||
app.use("/download", downloadRoute);
|
app.use("/download", downloadRoute);
|
||||||
app.use("/info", infoRoute);
|
app.use("/info", infoRoute);
|
||||||
|
|
||||||
// route GET / de base
|
// Route GET / qui sert la page principale index.html
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
debug("Requete GET / servie");
|
debug("Requete GET / servie");
|
||||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
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() {
|
function startServer() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
logger.info("Demarrage du serveur Express...");
|
logger.info("Demarrage du serveur Express...");
|
||||||
const serverInstance = app.listen(8080, () => {
|
const serverInstance = app.listen(8080, () => {
|
||||||
logger.info("Serveur Express pret sur http://localhost: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) => {
|
serverInstance.on("error", (err) => {
|
||||||
logger.error("Erreur serveur Express :", 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
|
// Gestion propre de la fermeture du process pour logger la fin de session
|
||||||
// process.on("exit", () => {
|
process.on("SIGINT", () => { // Capture Ctrl+C (interruption)
|
||||||
// logSessionEnd();
|
|
||||||
// });
|
|
||||||
process.on("SIGINT", () => { // Ctrl+C
|
|
||||||
logSessionEnd();
|
logSessionEnd();
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
process.on("SIGTERM", () => { // kill
|
process.on("SIGTERM", () => { // Capture kill ou arrêt du process
|
||||||
logSessionEnd();
|
logSessionEnd();
|
||||||
process.exit();
|
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 };
|
module.exports = { startServer };
|
||||||
|
|||||||
Reference in New Issue
Block a user