From c9a48ec64ce9d84eb2a92052388301250460ea2a Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:06:19 +0100 Subject: [PATCH] Using MVC Model + AutoDeteck Package + Fix Download/Info --- config.js | 3 +- package.json | 2 +- server/controller/download.controller.js | 46 ++++++++++ server/controller/info.controller.js | 49 +++++++++++ server/helpers/buildArgs.js | 9 +- server/helpers/parseInfo.js | 39 +++++++++ server/helpers/path.js | 9 +- server/helpers/validation.js | 16 ++-- server/logger.js | 2 +- server/routes/download.js | 107 ----------------------- server/routes/download.route.js | 40 +++++++++ server/routes/info.js | 101 --------------------- server/routes/info.route.js | 7 ++ server/server.js | 13 ++- server/services/download.services.js | 46 ++++++++++ server/services/info.services.js | 51 +++++++++++ 16 files changed, 313 insertions(+), 227 deletions(-) create mode 100644 server/controller/download.controller.js create mode 100644 server/controller/info.controller.js create mode 100644 server/helpers/parseInfo.js delete mode 100644 server/routes/download.js create mode 100644 server/routes/download.route.js delete mode 100644 server/routes/info.js create mode 100644 server/routes/info.route.js create mode 100644 server/services/download.services.js create mode 100644 server/services/info.services.js diff --git a/config.js b/config.js index f795b4d..e460608 100644 --- a/config.js +++ b/config.js @@ -1,9 +1,10 @@ const packageJson = require("./package.json"); +const { app } = require("electron"); module.exports = { version: packageJson.version, applicationPort: "8787", debugMode: true, - localMode: false, + localMode: !app.isPackaged, DiscordRPCID: "1410934537051181146", } \ No newline at end of file diff --git a/package.json b/package.json index 88375e4..bb240f6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "description": "Freedom Loader", "main": "main.js", "scripts": { - "start": "electron .", + "start": "electron --trace-warnings .", "dist": "electron-builder" }, "dependencies": { diff --git a/server/controller/download.controller.js b/server/controller/download.controller.js new file mode 100644 index 0000000..2f57699 --- /dev/null +++ b/server/controller/download.controller.js @@ -0,0 +1,46 @@ +const { fetchDownload } = require("../services/download.services"); +const { logger } = require("../logger"); +const { isValidUrl, isSafePath } = require("../helpers/validation"); +const { buildYtDlpArgs } = require("../helpers/buildArgs"); +const { notifyDownloadFinished } = require("../helpers/notify"); + +const listeners = []; + +async function downloadController(req, res) { + try { + const options = { + url: req.body.url, + audioOnly: req.body.audioOnly === "1", + quality: req.body.quality || "best", + outputFolder: req.body.savePath || undefined, + }; + + if (!options.url || !isValidUrl(options.url)) return res.status(400).send("❌ URL invalide !"); + if (options.outputFolder && !isSafePath(options.outputFolder)) return res.status(400).send("❌ Chemin de sauvegarde non autorisé."); + + const filePath = await fetchDownload(options, listeners); + notifyDownloadFinished(filePath); + res.send("✅ Téléchargement terminé !"); + } catch (err) { + logger.error(`Erreur serveur dans /download : ${err.message}`); + res.status(500).send(`❌ ${err.message}`); + } +} + +function progressController(req, res) { + res.set({ + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }); + + const sendProgress = percent => res.write(`data: ${percent}\n\n`); + listeners.push(sendProgress); + + req.on('close', () => { + listeners.splice(listeners.indexOf(sendProgress), 1); + res.end(); + }); +} + +module.exports = { downloadController, progressController }; diff --git a/server/controller/info.controller.js b/server/controller/info.controller.js new file mode 100644 index 0000000..d7fb895 --- /dev/null +++ b/server/controller/info.controller.js @@ -0,0 +1,49 @@ +const express = require("express"); +const router = express.Router(); + +const { fetchInfo } = require("../services/info.services"); +const { parseVideo, parsePlaylist } = require("../helpers/parseInfo"); +const { logger } = require("../logger"); +const { isValidUrl } = require("../helpers/validation"); + +async function infoController(req, res){ + + const url = req.body.url || req.query.url; // Gérer POST et GET + + + /* Si pas d'url ou url invalide*/ + if (!url || !isValidUrl(url)) return res.status(400).send("❌ URL invalide ou manquante"); + + logger.info(`Requête /info reçue par le controller. URL: ${url}`); + + if (url.includes("?list")) { + logger.info("Type de contenu: Playlist") + + } else{ + logger.info("Type de contenu: Vidéo") + } + + try { + const data = await fetchInfo(url); + + if (data._type === "playlist") { + + const playlist = parsePlaylist(data); + logger.info(`Playlist détectée : ${playlist.title} (${playlist.count} vidéos)`); + return res.json(playlist); + + } else{ + const video = parseVideo(data); + logger.info(`Vidéo unique récupérée : ${video.title}`); + return res.json(video); + } + + + + } catch (err) { + return res.status(500).send(`❌ ${err.message}`); + } + +} + +module.exports = {infoController}; \ No newline at end of file diff --git a/server/helpers/buildArgs.js b/server/helpers/buildArgs.js index 0d77bbe..6d16353 100644 --- a/server/helpers/buildArgs.js +++ b/server/helpers/buildArgs.js @@ -1,8 +1,11 @@ const path = require("path"); -const getUserBrowser = require("./getBrowser") +const getUserBrowser = require("./getBrowser"); +const { ffmpegPath, denoPath} = require("./path"); + function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) { + const args = [ "--cookies-from-browser", `${getUserBrowser()}`, "--no-continue", @@ -12,9 +15,9 @@ function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) { "--concurrent-fragments", "8", "--retries", "10", "--fragment-retries", "10", - "--ffmpeg-location", path.join(process.resourcesPath, "ffmpeg.exe"), + "--ffmpeg-location", ffmpegPath, "--extractor-args","youtube:player_client=default", - "--js-runtimes", `deno:${path.join(process.resourcesPath, "deno.exe")}` + "--js-runtimes", `deno:${denoPath}` ]; if (audioOnly) args.push("-f", "bestaudio", "--extract-audio", "--audio-format", "mp3"); diff --git a/server/helpers/parseInfo.js b/server/helpers/parseInfo.js new file mode 100644 index 0000000..ec258be --- /dev/null +++ b/server/helpers/parseInfo.js @@ -0,0 +1,39 @@ +const { logger } = require("../logger"); + +function parseVideo(data) { + + // logger.info(`Avant parse: ${JSON.stringify(data, null, 2)}`); + + return { + type: "video", + // id: data.id, + // title: data.title, + // url: data.webpage_url, + // duration: data.duration, + // thumbnail: data.thumbnail, + // uploader: data.uploader + ...data + }; +} + +function parsePlaylist(data) { + + // logger.info(`Avant parse: ${JSON.stringify(data, null, 2)}`); + + return { + type: "playlist", + title: data.title || data.id, + channel: data.uploader, + count: data.entries.length, + videos: (data.entries || []).map(v => ({ + id: v.id, + title: v.title, + url: v.webpage_url, + duration: v.duration, + thumbnail: v.thumbnail, + uploader: v.uploader + })) + }; +} + +module.exports = { parseVideo, parsePlaylist }; diff --git a/server/helpers/path.js b/server/helpers/path.js index 6056ccd..3de4789 100644 --- a/server/helpers/path.js +++ b/server/helpers/path.js @@ -5,19 +5,22 @@ const config = require("../../config"); let userYtDlp; let ffmpegPath; +let denoPath; const sourceYtDlp = path.join(process.resourcesPath, "yt-dlp.exe"); if (config.localMode) { userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe"); - ffmpegPath = path.join(__dirname, "../../ressources/ffmpeg.exe"); + ffmpegPath = path.join(__dirname, "../../ressources"); // <- contient ffmpeg.exe et ffprobe.exe + denoPath = path.join(__dirname, "../../ressources/deno.exe"); } else { userYtDlp = path.join(app.getPath("userData"), "yt-dlp.exe"); - ffmpegPath = path.join(process.resourcesPath, "ffmpeg.exe"); + ffmpegPath = path.join(process.resourcesPath, "ressources"); // <- ici aussi + denoPath = path.join(process.resourcesPath, "deno.exe"); if (!fs.existsSync(userYtDlp)) { fs.copyFileSync(sourceYtDlp, userYtDlp); } } -module.exports = { userYtDlp, sourceYtDlp, ffmpegPath }; +module.exports = { userYtDlp, ffmpegPath, denoPath }; diff --git a/server/helpers/validation.js b/server/helpers/validation.js index 161a5eb..b6ede30 100644 --- a/server/helpers/validation.js +++ b/server/helpers/validation.js @@ -1,12 +1,14 @@ const path = require("path"); -function isValidUrl(url) { - try { - new URL(url); - return true; - } catch { - return false; - } +function isValidUrl(url) { + try { + new URL(url); + return true; + + } catch { + return false; + } + } function isSafePath(folder) { diff --git a/server/logger.js b/server/logger.js index 69b36d9..5c5d70e 100644 --- a/server/logger.js +++ b/server/logger.js @@ -8,7 +8,7 @@ const config = require("../config") // Dossier de logs Windows const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs"); -// Création du dossier si nécessaire +// Création du dossier "logs" si nécessaire fs.mkdirSync(logDir, { recursive: true }); const logFormat = format.combine( diff --git a/server/routes/download.js b/server/routes/download.js deleted file mode 100644 index 6bb4044..0000000 --- a/server/routes/download.js +++ /dev/null @@ -1,107 +0,0 @@ -const { execFile } = require("child_process"); -const express = require("express"); -const router = express.Router(); -const path = require("path"); -const fs = require("fs"); - -const { logger } = require("../logger"); -const { isValidUrl, isSafePath } = require("../helpers/validation"); -const { buildYtDlpArgs } = require("../helpers/buildArgs"); -const { notifyDownloadFinished } = require("../helpers/notify"); -const { userYtDlp } = require("../helpers/path"); - -const listeners = []; - -router.post("/", (req, res) => { - try { - const options = { - url: req.body.url, - audioOnly: req.body.audioOnly === "1", - quality: req.body.quality || "best", - outputFolder: req.body.savePath || path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader"), - }; - - // Vérifications - if (!options.url || !isValidUrl(options.url)) return res.status(400).send("❌ URL invalide !"); - if (!isSafePath(options.outputFolder)) return res.status(400).send("❌ Chemin de sauvegarde non autorisé."); - - // Création dossier si inexistant - fs.mkdirSync(options.outputFolder, { recursive: true }); - - // Construire les arguments yt-dlp - const args = buildYtDlpArgs(options); - logger.info(`[yt-dlp args] ${args.join(" ")}`); - - const child = execFile(userYtDlp, args); - - // Gestion erreurs - child.on("error", err => { - logger.error(`Erreur yt-dlp : ${err.message}`); - res.status(500).send(`❌ Erreur yt-dlp : ${err.message}`); - }); - - // Téléchargement terminé - child.on("close", code => { - if (code === 0) { - notifyDownloadFinished(options.outputFolder); - - // signal SSE fin de playlist - listeners.forEach(fn => fn("done")); - - res.send("✅ Téléchargement terminé !"); - } else { - res.status(500).send(`❌ yt-dlp a échoué avec le code : ${code}`); - listeners.forEach(fn => fn("done")); - } - }); - - child.stdout.on("data", data => { - data.toString().split("\n").forEach(line => line.trim() && logger.info(`[yt-dlp stdout] ${line}`)); - }); - - child.stderr.on("data", data => { - data.toString().split("\n").forEach(line => line.trim() && logger.error(`[yt-dlp stderr] ${line}`)); - }); - - - child.stdout.on("data", data => { - const lines = data.toString().split("\n"); - lines.forEach(line => { - // reset progress si nouveau fichier - if (line.startsWith("[download] Destination:")) { - listeners.forEach(fn => fn("reset")); // tu peux envoyer un signal spécial - } - - // envoyer le % classique - const match = line.match(/\[download\]\s+(\d+\.\d+)%/); - if (match) { - const percent = parseFloat(match[1]); - listeners.forEach(fn => fn(percent)); - } - }); - }); - - } catch (err) { - logger.error(`Erreur serveur dans /download : ${err.message}`); - res.status(500).send(`Erreur serveur : ${err.message}`); - } -}); - -// SSE endpoint -router.get("/progress", (req, res) => { - res.set({ - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - }); - - const sendProgress = percent => res.write(`data: ${percent}\n\n`); - listeners.push(sendProgress); - - req.on('close', () => { - listeners.splice(listeners.indexOf(sendProgress), 1); - res.end(); - }); -}); - -module.exports = router; diff --git a/server/routes/download.route.js b/server/routes/download.route.js new file mode 100644 index 0000000..5f5ffc3 --- /dev/null +++ b/server/routes/download.route.js @@ -0,0 +1,40 @@ +const express = require("express"); +const router = express.Router(); +const { fetchDownload } = require("../services/download.services"); + +const listeners = []; + +router.post("/", async (req, res) => { + const options = { + url: req.body.url, + audioOnly: req.body.audioOnly === "1", + quality: req.body.quality || "best", + outputFolder: req.body.savePath || null + }; + + try { + await fetchDownload(options, listeners); + res.send(`✅ Téléchargement terminé`); + } catch (err) { + res.status(500).send(`❌ ${err.message}`); + } +}); + +// SSE pour la barre de progression +router.get("/progress", (req, res) => { + res.set({ + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }); + + const sendProgress = percent => res.write(`data: ${percent}\n\n`); + listeners.push(sendProgress); + + req.on("close", () => { + listeners.splice(listeners.indexOf(sendProgress), 1); + res.end(); + }); +}); + +module.exports = router; diff --git a/server/routes/info.js b/server/routes/info.js deleted file mode 100644 index 372d855..0000000 --- a/server/routes/info.js +++ /dev/null @@ -1,101 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const { execFile } = require("child_process"); -const { logger } = require("../logger"); -const { userYtDlp } = require("../helpers/path"); -const getUserBrowser = require("../helpers/getBrowser"); -const path = require("path"); - -function isValidUrl(url) { - try { - new URL(url); - return true; - } catch { - return false; - } -} - -router.post("/", (req, res) => { - const { url } = req.body; - if (!url || !isValidUrl(url)) return res.status(400).send("❌ URL invalide ou manquante"); - - logger.info(`Requête /info reçue pour ${url}`); - - const args = [ - "--no-js-runtimes", - "--js-runtimes", - `deno:${path.join(process.resourcesPath, "deno.exe")}`, - "--dump-single-json", - "--ignore-errors", - "--yes-playlist", - "--cookies-from-browser", - `${getUserBrowser()}`, - "--extractor-args", - "youtube:player_client=default", - "--ignore-no-formats-error", - url, - ]; - console.log("JS RUNTIME:", `deno:${path.join(process.resourcesPath, "deno.exe")}`); - - execFile(userYtDlp, args, { timeout: 30_000, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => { - - if (stderr) { - const lower = stderr.toLowerCase(); - if (lower.includes("sign in to confirm") || lower.includes("failed to decrypt") || lower.includes("could not copy")) { - const browser = getUserBrowser(); - return res.status(400).send( - `❌ Impossible d'extraire les cookies depuis ${browser}. Connectez-vous dans ce navigateur et réessayez.` - ); - } - } - - if (error) { - logger.error(`Erreur yt-dlp: ${error.message}`); - if (stderr) logger.debug(`stderr: ${stderr}`); - return res.status(500).send("❌ Impossible de récupérer les infos."); - } - - if (!stdout) return res.status(500).send("❌ Aucune donnée reçue."); - - try { - const data = JSON.parse(stdout); - - if (data._type === "playlist") { - const playlistPayload = { - type: "playlist", - title: data.title || data.id, - channel: data.uploader, - count: data.entries.length, - videos: (data.entries || []).map(v => ({ - id: v.id, - title: v.title, - url: v.webpage_url, - duration: v.duration, - thumbnail: v.thumbnail, - uploader: v.uploader - })) - }; - logger.info(`Playlist détectée : ${playlistPayload.title} (${playlistPayload.count} vidéos)`); - return res.json(playlistPayload); - } - - // Vidéo unique - if (!data.title) { - return res.status(500).send("❌ Impossible de récupérer les infos pour cette vidéo."); - - } - - logger.info(`Vidéo unique récupérée : ${data.title}`); - res.json({ type: "video", ...data }); - - } catch (e) { - // fallback aussi si JSON est illisible - logger.error(`Erreur parsing JSON: ${e.message}`); - return res.status(500).send("❌ Impossible de récupérer les infos (JSON illisible)."); - - } - - }); -}); - -module.exports = router; diff --git a/server/routes/info.route.js b/server/routes/info.route.js new file mode 100644 index 0000000..f7465fe --- /dev/null +++ b/server/routes/info.route.js @@ -0,0 +1,7 @@ +const express = require("express"); +const router = express.Router(); +const { infoController } = require("../controller/info.controller"); + +router.post("/", infoController); + +module.exports = router; \ No newline at end of file diff --git a/server/server.js b/server/server.js index 2514b2c..9756ec0 100644 --- a/server/server.js +++ b/server/server.js @@ -6,11 +6,18 @@ const config = require("../config"); const { execFile } = require("child_process"); const { userYtDlp } = require("./helpers/path"); +const {ffmpegPath, denoPath} = require("./helpers/path") + const app = express(); +console.log("ffmpegPath:", ffmpegPath); +console.log("denoPath:", denoPath); +console.log("Files in ffmpegPath:", fs.readdirSync(ffmpegPath)); + + +app.use(express.json()); // Dossier de téléchargement const outputFolder = path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader"); - // Création du dossier si nécessaire try { fs.mkdirSync(outputFolder, { recursive: true }); @@ -31,8 +38,8 @@ app.use(express.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname, "../public"))); // Routes -app.use("/download", require("./routes/download")); -app.use("/info", require("./routes/info")); +app.use("/download", require("./routes/download.route")); +app.use("/info", require("./routes/info.route")); app.get("/", (req, res) => { res.sendFile(path.join(__dirname, "../public/index.html")); diff --git a/server/services/download.services.js b/server/services/download.services.js new file mode 100644 index 0000000..6edce7f --- /dev/null +++ b/server/services/download.services.js @@ -0,0 +1,46 @@ +const { execFile } = require("child_process"); +const { userYtDlp } = require("../helpers/path"); +const path = require("path"); +const fs = require("fs"); +const { logger } = require("../logger"); +const { buildYtDlpArgs } = require("../helpers/buildArgs"); + +function fetchDownload(options, listeners) { + + return new Promise((resolve, reject) => { + const outputFolder = options.outputFolder || path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader"); + fs.mkdirSync(outputFolder, { recursive: true }); + + const args = buildYtDlpArgs({ ...options, outputFolder }); + logger.info(`[yt-dlp args] ${args.join(" ")}`); + + const child = execFile(userYtDlp, args); + + child.on("error", err => reject(new Error(`Erreur yt-dlp : ${err.message}`))); + + child.on("close", code => { + listeners.forEach(fn => fn("done")); + if (code === 0) resolve(outputFolder); + else reject(new Error(`yt-dlp a échoué avec le code : ${code}`)); + }); + + + child.stdout.on("data", data => { + data.toString().split("\n").forEach(line => { + if (!line.trim()) return; + logger.info(`[yt-dlp stdout] ${line}`); + + /* Barre de Chargement*/ + if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset")); + const match = line.match(/\[download\]\s+(\d+\.\d+)%/); + if (match) listeners.forEach(fn => fn(parseFloat(match[1]))); + }); + }); + + child.stderr.on("data", data => { + data.toString().split("\n").forEach(line => line.trim() && logger.error(`[yt-dlp stderr] ${line}`)); + }); + }); +} + +module.exports = { fetchDownload }; diff --git a/server/services/info.services.js b/server/services/info.services.js new file mode 100644 index 0000000..010c776 --- /dev/null +++ b/server/services/info.services.js @@ -0,0 +1,51 @@ +const { execFile } = require("child_process"); +const { userYtDlp, denoPath } = require("../helpers/path"); +const getUserBrowser = require("../helpers/getBrowser"); +const { logger } = require("../logger"); + +function fetchInfo(url) { + const args = [ + "--js-runtimes", `deno:${denoPath}`, + "--dump-single-json", + "--ignore-errors", + "--yes-playlist", + "--cookies-from-browser",`${getUserBrowser()}`, + "--extractor-args","youtube:player_client=default", + "--ignore-no-formats-error", + ]; + + return new Promise((resolve, reject) => { + + execFile(userYtDlp,[...args, url], + { timeout: 30_000, // 30s, si jamais plus, abandon de la requête + maxBuffer: 10 * 1024 * 1024 }, // 10MO de réponse max (Par défaut: 200ko) + + (error, stdout, stderr) => { + + if (stderr) { + const lower = stderr.toLowerCase(); + if (lower.includes("sign in to confirm") || lower.includes("failed to decrypt") || lower.includes("could not copy")) { + return reject(new Error(`Impossible d'extraire les cookies depuis ${getUserBrowser()}. Connectez-vous dans ce navigateur et réessayez.`)); + } + } + + if (error) { + logger.error(`Erreur yt-dlp: ${error.message}`); + if (stderr) logger.debug(`stderr: ${stderr}`); + return reject(new Error("Impossible de récupérer les infos.")); + } + + if (!stdout) return reject(new Error("Aucune donnée reçue.")); + + try { + const data = JSON.parse(stdout); + resolve(data); + } catch (e) { + logger.error(`Erreur parsing JSON: ${e.message}`); + reject(new Error("JSON illisible.")); + } + }); + }); +} + +module.exports = { fetchInfo };