mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-07-29 18:25:47 +02:00
Using MVC Model + AutoDeteck Package + Fix Download/Info
This commit is contained in:
@@ -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",
|
||||||
}
|
}
|
||||||
@@ -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": {
|
||||||
|
|||||||
46
server/controller/download.controller.js
Normal file
46
server/controller/download.controller.js
Normal 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 };
|
||||||
49
server/controller/info.controller.js
Normal file
49
server/controller/info.controller.js
Normal 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};
|
||||||
@@ -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");
|
||||||
|
|||||||
39
server/helpers/parseInfo.js
Normal file
39
server/helpers/parseInfo.js
Normal 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 };
|
||||||
@@ -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 };
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ function isValidUrl(url) {
|
|||||||
try {
|
try {
|
||||||
new URL(url);
|
new URL(url);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSafePath(folder) {
|
function isSafePath(folder) {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
|
||||||
40
server/routes/download.route.js
Normal file
40
server/routes/download.route.js
Normal 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;
|
||||||
@@ -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;
|
|
||||||
7
server/routes/info.route.js
Normal file
7
server/routes/info.route.js
Normal 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;
|
||||||
@@ -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"));
|
||||||
|
|||||||
46
server/services/download.services.js
Normal file
46
server/services/download.services.js
Normal 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 };
|
||||||
51
server/services/info.services.js
Normal file
51
server/services/info.services.js
Normal 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 };
|
||||||
Reference in New Issue
Block a user