Using MVC Model + AutoDeteck Package + Fix Download/Info

This commit is contained in:
MasterAcnolo
2025-11-15 15:06:19 +01:00
parent e6419a9759
commit c9a48ec64c
16 changed files with 313 additions and 227 deletions

View File

@@ -1,9 +1,10 @@
const packageJson = require("./package.json"); const packageJson = require("./package.json");
const { app } = require("electron");
module.exports = { module.exports = {
version: packageJson.version, version: packageJson.version,
applicationPort: "8787", applicationPort: "8787",
debugMode: true, debugMode: true,
localMode: false, localMode: !app.isPackaged,
DiscordRPCID: "1410934537051181146", DiscordRPCID: "1410934537051181146",
} }

View File

@@ -6,7 +6,7 @@
"description": "Freedom Loader", "description": "Freedom Loader",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"start": "electron .", "start": "electron --trace-warnings .",
"dist": "electron-builder" "dist": "electron-builder"
}, },
"dependencies": { "dependencies": {

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
const path = require("path"); const path = require("path");
const getUserBrowser = require("./getBrowser") const getUserBrowser = require("./getBrowser");
const { ffmpegPath, denoPath} = require("./path");
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) { function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
const args = [ const args = [
"--cookies-from-browser", `${getUserBrowser()}`, "--cookies-from-browser", `${getUserBrowser()}`,
"--no-continue", "--no-continue",
@@ -12,9 +15,9 @@ function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
"--concurrent-fragments", "8", "--concurrent-fragments", "8",
"--retries", "10", "--retries", "10",
"--fragment-retries", "10", "--fragment-retries", "10",
"--ffmpeg-location", path.join(process.resourcesPath, "ffmpeg.exe"), "--ffmpeg-location", ffmpegPath,
"--extractor-args","youtube:player_client=default", "--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"); if (audioOnly) args.push("-f", "bestaudio", "--extract-audio", "--audio-format", "mp3");

View File

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

View File

@@ -5,19 +5,22 @@ const config = require("../../config");
let userYtDlp; let userYtDlp;
let ffmpegPath; let ffmpegPath;
let denoPath;
const sourceYtDlp = path.join(process.resourcesPath, "yt-dlp.exe"); const sourceYtDlp = path.join(process.resourcesPath, "yt-dlp.exe");
if (config.localMode) { if (config.localMode) {
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe"); 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 { } else {
userYtDlp = path.join(app.getPath("userData"), "yt-dlp.exe"); 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)) { if (!fs.existsSync(userYtDlp)) {
fs.copyFileSync(sourceYtDlp, userYtDlp); fs.copyFileSync(sourceYtDlp, userYtDlp);
} }
} }
module.exports = { userYtDlp, sourceYtDlp, ffmpegPath }; module.exports = { userYtDlp, ffmpegPath, denoPath };

View File

@@ -1,12 +1,14 @@
const path = require("path"); const path = require("path");
function isValidUrl(url) { function isValidUrl(url) {
try { try {
new URL(url); new URL(url);
return true; return true;
} catch {
return false; } catch {
} return false;
}
} }
function isSafePath(folder) { function isSafePath(folder) {

View File

@@ -8,7 +8,7 @@ const config = require("../config")
// Dossier de logs Windows // Dossier de logs Windows
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs"); 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 }); fs.mkdirSync(logDir, { recursive: true });
const logFormat = format.combine( const logFormat = format.combine(

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
const express = require("express");
const router = express.Router();
const { infoController } = require("../controller/info.controller");
router.post("/", infoController);
module.exports = router;

View File

@@ -6,11 +6,18 @@ const config = require("../config");
const { execFile } = require("child_process"); const { execFile } = require("child_process");
const { userYtDlp } = require("./helpers/path"); const { userYtDlp } = require("./helpers/path");
const {ffmpegPath, denoPath} = require("./helpers/path")
const app = express(); 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 // Dossier de téléchargement
const outputFolder = path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader"); const outputFolder = path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader");
// Création du dossier si nécessaire // Création du dossier si nécessaire
try { try {
fs.mkdirSync(outputFolder, { recursive: true }); fs.mkdirSync(outputFolder, { recursive: true });
@@ -31,8 +38,8 @@ app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, "../public"))); app.use(express.static(path.join(__dirname, "../public")));
// Routes // Routes
app.use("/download", require("./routes/download")); app.use("/download", require("./routes/download.route"));
app.use("/info", require("./routes/info")); app.use("/info", require("./routes/info.route"));
app.get("/", (req, res) => { app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "../public/index.html")); res.sendFile(path.join(__dirname, "../public/index.html"));

View File

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

View File

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