Merge pull request #1 from MasterAcnolo/Développement-Branch

[PR] 1.0.1
This commit is contained in:
MasterAcnolo
2025-07-01 15:21:11 +02:00
committed by GitHub
18 changed files with 756 additions and 1870 deletions

1
.gitignore vendored
View File

@@ -6,4 +6,3 @@
yt-dlp.exe
forge.config.js

View File

@@ -1,6 +1,12 @@
# Freedom Loader
![Static Badge](https://img.shields.io/badge/Version-1.0.0-blue?style=flat&logo=Test)
<!-- Release -->
![Static Badge](https://img.shields.io/badge/Release-1.0.0-blue?style=flat&logo=Test)
<!-- Build -->
![Static Badge](https://img.shields.io/badge/Build-1.0.1--beta-yellow?style=flat&logo=Test)
> This project aims to enable free, open-source, and unrestricted downloading of Video/Audio content. Eventually, it will allow users to choose

View File

@@ -1,6 +1,12 @@
# Freedom Loader
![Static Badge](https://img.shields.io/badge/Version-1.0.0-blue?style=flat&logo=Test)
<!-- Release -->
![Static Badge](https://img.shields.io/badge/Release-1.0.0-blue?style=flat&logo=Test)
<!-- Build -->
![Static Badge](https://img.shields.io/badge/Build-1.0.1--beta-yellow?style=flat&logo=Test)
> Ce projet a pour but de permettre le téléchargement de contenu Vidéo/Audio de manière gratuite, open source et libre. Il permettra à terme de choisir

44
forge.config.js Normal file
View File

@@ -0,0 +1,44 @@
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
module.exports = {
packagerConfig: {
asar: true,
},
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {},
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
},
{
name: '@electron-forge/maker-deb',
config: {},
},
{
name: '@electron-forge/maker-rpm',
config: {},
},
],
plugins: [
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {},
},
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
};

View File

@@ -24,17 +24,22 @@ let mainWindow;
async function createWindow() {
logger.info("Creation de la fenetre...");
if (mainWindow) {
logger.warn("La fenetre existe deja, pas de nouvelle creation");
return;
}
mainWindow = new BrowserWindow({
width: 1000,
height: 700,
title: "Freedom Loader",
width: 750,
height: 800,
minWidth: 750,
minHeight: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
// titleBarStyle: 'hidden',
});
try {

2308
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "freedom-loader",
"productName": "Freedom Loader",
"version": "1.0.0",
"version": "1.0.1",
"author": "MasterAcnolo",
"description": "Freedom Loader, parce que nous le valons bien",
"main": "main.js",

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -0,0 +1,3 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" transform="scale(64)" fill="#1B1F23"/>
</svg>

After

Width:  |  Height:  |  Size: 968 B

View File

@@ -41,8 +41,13 @@
<!-- <input type="checkbox" name="audioOnly" value="1"> Audio seulement (MP3) -->
<select name="quality">
<option value="best">Meilleure qualité</option>
<option value="medium">Qualité moyenne</option>
<option value="worst">Qualité minimale</option>
<option value="1080">1080p</option>
<option value="720">720p</option>
<option value="480">480p</option>
</select>
<button type="submit">Télécharger</button>
</form>

View File

@@ -1,5 +1,3 @@
/* From Uiverse.io by bociKond */
/* Hide the default checkbox */
.checkbox-mp3 input {
position: absolute;
opacity: 0;
@@ -16,9 +14,8 @@
user-select: none;
}
/* Create a custom checkbox */
.checkmark {
--clr: #0B6E4F;
--clr: #3B82F6 ;
position: relative;
top: 0;
left: 0;
@@ -29,26 +26,22 @@
transition: 300ms;
}
/* When the checkbox is checked, add a blue background */
.checkbox-mp3 input:checked ~ .checkmark {
background-color: var(--clr);
border-radius: .5rem;
animation: pulse 500ms ease-in-out;
}
/* Create the checkmark/indicator (hidden when not checked) */
.checkmark:after {
content: "";
position: absolute;
display: none;
}
/* Show the checkmark when checked */
.checkbox-mp3 input:checked ~ .checkmark:after {
display: block;
}
/* Style the checkmark/indicator */
.container .checkmark:after {
left: 0.45em;
top: 0.25em;
@@ -61,7 +54,7 @@
@keyframes pulse {
0% {
box-shadow: 0 0 0 #0B6E4F90;
box-shadow: 0 0 0 var(--checkbox-pulse-color);
rotate: 20deg;
}
@@ -70,11 +63,11 @@
}
75% {
box-shadow: 0 0 0 10px #0B6E4F60;
box-shadow: 0 0 0 10px var(--checkbox-pulse-color);
}
100% {
box-shadow: 0 0 0 13px #0B6E4F30;
box-shadow: 0 0 0 13px var(--checkbox-pulse-color);
rotate: 0;
}
}

View File

@@ -4,7 +4,7 @@
margin-left: auto;
margin-right: auto;
padding: 1.5em 2em;
background-color: #fff;
background-color: var(--infos-box-color);
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
color: #222;

View File

@@ -3,7 +3,8 @@
@import url("layout/ctr-infos.css");
@import url("layout/header.css");
@import url("layout/videoinfo.css");
@import url("button/checkbox.css");
@import url("components/checkbox.css");
@import url("variables.css");
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
@@ -11,7 +12,7 @@ html,body{
margin: 0;
padding: 0;
scroll-behavior: smooth;
/* outline: 2px solid red; */
/* outline: 2px solid red; */ /*<-- Utile pour le début ça */
font-family: "Poppins", sans-serif;
font-weight: 500;
font-style: normal;
@@ -19,14 +20,14 @@ html,body{
body {
margin: 0;
/* background-color: #f5f5f5; */
background-image: linear-gradient(to left top, #838383, #a1a1a1, #bfbfbf, #dfdfdf, #ffffff);
background-color: var(--background-color);
/* background-image: linear-gradient(to left top, #838383, #a1a1a1, #bfbfbf, #dfdfdf, #ffffff);
background-size: cover;
background-position: center;
background-attachment: fixed;
background-attachment: fixed; */
width: 100%;
height: 100%;
color: #333;
color: var(--default-text-color);
display: flex;
justify-content: center;
align-items: center;
@@ -34,9 +35,15 @@ body {
flex-direction: column;
align-items: center;
}
header p{
color: var(--subtitle-color);
}
.checkbox-inline{
color: var(--mp3-text-color);
}
/* #videoInfo {
margin-top: 100px;

View File

@@ -0,0 +1,11 @@
:root{
--background-color : #0F172A;
--infos-box-color: #f1f1f1 ;
--title-color: ;
--subtitle-color : #a3aab4;
--mp3-text-color: #0F172A;
--default-text-color: #F8FAFC;
--checkbox-pulse-color:#9bbcf1;
}

View File

@@ -18,44 +18,50 @@
const { createLogger, format, transports } = require("winston");
const DailyRotateFile = require("winston-daily-rotate-file");
const fs = require("fs");
const path = require("path");
const fs = require("fs"); // pour vérifier/créer le dossier de logs
const path = require("path"); // pour gérer proprement les chemins
// on définit le dossier de logs
const logDir = path.join(__dirname, "../logs");
// on le crée si inexistant
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir);
// fonction pour générer une ligne de début de session
function getSessionStartLine() {
const now = new Date().toISOString();
return `--- Démarrage de la session : ${now} ---`;
}
// fonction pour générer une ligne de fin de session
function getSessionEndLine() {
const now = new Date().toISOString();
return `--- Fin de la session : ${now} ---`;
}
// configuration principale de Winston
const logger = createLogger({
level: "info",
level: "info", // niveau de log minimum (info, warn, error, etc.)
format: format.combine(
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), // timestamp lisible
format.printf(({ timestamp, level, message }) =>
`${timestamp} | ${level.toUpperCase()} | ${message}`
`${timestamp} | ${level.toUpperCase()} | ${message}` // format de ligne de log
)
),
transports: [
new DailyRotateFile({
dirname: logDir,
filename: "combined-%DATE%.log",
datePattern: "YYYY-MM-DD",
zippedArchive: false, // tu peux mettre true si tu veux compresser
maxFiles: "14d", // conserve les logs des 14 derniers jours
dirname: logDir, // dossier de logs
filename: "LOGS-%DATE%.log", // nom de fichier journalier
datePattern: "YYYY-MM-DD", // pattern de rotation
zippedArchive: false, // true = compression des archives
maxFiles: "14d", // garde 14 jours dhistorique
format: format.combine(
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
format.printf(({ timestamp, level, message }) =>
`${timestamp} | ${level.toUpperCase()} | ${message}`
)
),
options: { flags: "a" },
options: { flags: "a" }, // mode append
}),
new transports.Console({
format: format.combine(
@@ -69,14 +75,17 @@ const logger = createLogger({
],
});
// fonction d'utilitaire pour marquer le début de la session
function logSessionStart() {
logger.info(getSessionStartLine());
}
// pour la fin de la session
function logSessionEnd() {
logger.info(getSessionEndLine());
}
// exporte le logger et les helpers pour les autres modules
module.exports = {
logger,
logSessionStart,

View File

@@ -16,90 +16,150 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const express = require("express");
const router = express.Router();
const { execFile } = require("child_process");
const path = require("path");
const fs = require("fs");
const express = require("express"); // importe le framework Express
const router = express.Router(); // crée un routeur Express pour modulariser les routes
const { execFile } = require("child_process"); // permet dexécuter un binaire externe sans passer par un shell
const path = require("path"); // module Node.js pour gérer les chemins de fichiers
const fs = require("fs"); // module Node.js pour manipuler le système de fichiers
const logger = require("../logger").logger; // importe ton logger Winston
const logger = require("../logger").logger; // récupère l'instance de logger Winston personnalisée
// chemin vers lexécutable yt-dlp packagé avec lappli
const ytDlpPath = path.join(__dirname, '../../yt-dlp.exe');
// route POST sur la racine (ex: POST /download)
router.post("/", (req, res) => {
try {
// récupère les options envoyées depuis le frontend (via formulaire ou requête AJAX)
const options = {
url: req.body.url,
audioOnly: req.body.audioOnly === "1",
quality: req.body.quality || "best",
subtitles: req.body.subs === "1",
url: req.body.url, // URL à télécharger
audioOnly: req.body.audioOnly === "1", // conversion audio uniquement (mp3)
quality: req.body.quality || "best", // qualité vidéo à télécharger
subtitles: req.body.subs === "1", // récupérer les sous-titres anglais
};
if (!options.url) {
// si lURL est absente, on log et on renvoie 400
logger.warn("Requête POST /download sans URL");
return res.status(400).send("❌ URL manquante !");
}
// Récupérer le chemin Freedom Loader Output depuis app.locals
// on récupère le dossier de sortie depuis app.locals (défini ailleurs dans le serveur)
const outputFolder = req.app.locals.outputFolder;
// s'il n'existe pas, on le crée automatiquement
if (!fs.existsSync(outputFolder)) {
fs.mkdirSync(outputFolder, { recursive: true });
logger.info("Dossier Freedom Loader Output créé à la volée dans Téléchargements.");
}
// Construire le template de sortie
// template du nom de fichier de sortie (ex: MonTitre.mp4 ou MonTitre.mp3)
const outputTemplate = path.join(outputFolder, "%(title)s.%(ext)s");
// Construire les arguments pour execFile
// tableau d'arguments pour yt-dlp
const args = [];
if (options.audioOnly) {
// si audioOnly, on force extraction audio en mp3
args.push("--extract-audio", "--audio-format", "mp3");
}
if (options.subtitles) {
// si lutilisateur veut des sous-titres
args.push("--write-subs", "--sub-lang", "en");
}
args.push("-f", options.quality);
// // qualité souhaitée
// args.push("-f", options.quality);
// let format;
// if (options.quality === "best") {
// format = "bestvideo+bestaudio/best";
// } else if (options.quality === "worst") {
// format = "worstvideo+worstaudio/worst";
// } else {
// format = "best"; // fallback
// }
// args.push("-f", format);
args.push("--no-continue");
let format;
switch (options.quality) {
case "best":
format = "bestvideo+bestaudio/best";
break;
case "medium":
// qualité moyenne, ex: 720p vidéo + meilleur audio
format = "bestvideo[height<=720]+bestaudio/best[height<=720]";
break;
case "worst":
format = "worstvideo+worstaudio/worst";
break;
case "1080":
format = "bestvideo[height<=1080]+bestaudio/best[height<=1080]";
break;
case "720":
format = "bestvideo[height<=720]+bestaudio/best[height<=720]";
break;
case "480":
format = "bestvideo[height<=480]+bestaudio/best[height<=480]";
break;
default:
format = "best";
}
args.push("-f", format);
// modèle de sortie
args.push("-o", outputTemplate);
// enfin, l'URL à télécharger
args.push(options.url);
// log de la commande complète pour debug
logger.info(`Commande yt-dlp : ${ytDlpPath} ${args.join(" ")}`);
// exécution du binaire yt-dlp
const child = execFile(ytDlpPath, args);
// gestion des logs stdout de yt-dlp
child.stdout.on("data", (data) => {
data.toString().split("\n").forEach(line => {
if(line.trim()) logger.info(`[yt-dlp stdout] ${line.trim()}`);
});
});
// gestion des logs stderr de yt-dlp (les erreurs)
child.stderr.on("data", (data) => {
data.toString().split("\n").forEach(line => {
if(line.trim()) logger.error(`[yt-dlp stderr] ${line.trim()}`);
});
});
// si une erreur système survient (par ex : yt-dlp.exe introuvable)
child.on("error", (err) => {
logger.error(`Erreur lancement yt-dlp : ${err.message}`);
res.status(500).send(`❌ Erreur lors de l'exécution : ${err.message}`);
});
// callback quand le processus se termine
child.on("close", (code) => {
logger.info(`yt-dlp terminé avec code de sortie : ${code}`);
if(code === 0) {
// succès
res.send("✅ Téléchargement terminé !");
} else {
// yt-dlp a échoué
res.status(500).send(`❌ yt-dlp a échoué avec le code : ${code}`);
}
});
} catch (err) {
// en cas d'erreur JavaScript
logger.error(`Erreur serveur dans /download : ${err.message}`);
res.status(500).send(`Erreur serveur : ${err.message}`);
}
});
// on exporte ce routeur pour qu'il soit utilisé par l'app principale
module.exports = router;

View File

@@ -16,32 +16,40 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const express = require("express");
const router = express.Router();
const { execFile } = require("child_process");
const path = require("path");
const express = require("express"); // importe Express
const router = express.Router(); // crée un routeur Express indépendant
const { execFile } = require("child_process"); // permet dexécuter un programme externe
const path = require("path"); // module pour gérer les chemins de fichiers
// chemin vers le binaire yt-dlp embarqué
const ytDlpPath = path.join(__dirname, "../../yt-dlp.exe");
// route POST sur la racine de ce routeur
router.post("/", (req, res) => {
const url = req.body.url;
if (!url) return res.status(400).send("❌ URL manquante");
const url = req.body.url; // récupère l'URL depuis la requête POST
if (!url) return res.status(400).send("❌ URL manquante"); // validation simple
// exécute yt-dlp avec --dump-json pour récupérer les métadonnées de la vidéo
execFile(ytDlpPath, ["--dump-json", url], (error, stdout, stderr) => {
if (error) {
// gestion derreur : par exemple yt-dlp plante ou lURL est invalide
console.error("Erreur lors de l'exécution de yt-dlp :", error);
console.error("stderr :", stderr);
return res.status(500).send("❌ Impossible de récupérer les infos.");
}
try {
// on parse le JSON envoyé par yt-dlp
const info = JSON.parse(stdout);
// on renvoie lobjet JSON directement au frontend
res.json(info);
} catch (e) {
// le JSON est mal formé
console.error("Erreur lors du parsing JSON :", e);
res.status(500).send("❌ JSON illisible.");
}
});
});
// on exporte le routeur pour être utilisé dans app.js ou ailleurs
module.exports = router;

View File

@@ -16,59 +16,68 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const express = require("express");
const { exec } = require("child_process");
const fs = require("fs");
const path = require("path");
const express = require("express"); // framework web
const { exec } = require("child_process"); // si besoin dexécuter des commandes (pas utilisé ici directement)
const fs = require("fs"); // accès au système de fichiers
const path = require("path"); // gestion de chemins
const debug = require("debug")("freedom-loader:server");
// petit debugger coloré en console
const { logger, logSessionStart, logSessionEnd } = require("./logger");
// récupère notre logger Winston + helpers de session
const app = express();
const app = express(); // instancie l'application Express
// // Lancement d'une nouvelle session de log
// logSessionStart();
// logSessionStart(); // désactivé, mais pourrait logguer le début de session
// dossier de sortie
// on définit le dossier de téléchargement dans le dossier Téléchargements de lutilisateur
const downloadsPath = path.join(process.env.USERPROFILE, "Downloads");
const outputFolder = path.join(downloadsPath, "Freedom Loader Output");
// création du dossier sil nexiste pas
if (!fs.existsSync(outputFolder)) {
try {
fs.mkdirSync(outputFolder, { recursive: true });
logger.info("Dossier Freedom Loader Output cree dans Telechargements.");
} catch (err) {
logger.error("Impossible de creer le dossier :", err);
process.exit(1);
process.exit(1); // arrêt du programme en cas déchec
}
} else {
logger.info("Dossier Freedom Loader Output deja existant.");
}
// on rend ce dossier dispo via app.locals
app.locals.outputFolder = outputFolder;
// configuration pour parser les requêtes POST en urlencoded
app.use(express.urlencoded({ extended: true }));
// sert les fichiers statiques (frontend)
const staticPath = path.join(__dirname, "../public");
debug("Serveur statique sur", staticPath);
app.use(express.static(staticPath));
// routes
// enregistre les routes API
const infoRoute = require("./routes/info");
const downloadRoute = require("./routes/download");
debug("Routes /download et /info installees");
app.use("/download", downloadRoute);
app.use("/info", infoRoute);
// route GET / de base
app.get("/", (req, res) => {
debug("Requete GET / servie");
res.sendFile(path.join(__dirname, "../public/index.html"));
});
// fonction pour démarrer le serveur (Promise pour pouvoir await ailleurs)
function startServer() {
return new Promise((resolve, reject) => {
logger.info("Demarrage du serveur Express...");
const serverInstance = app.listen(8080, () => {
logger.info("Serveur Express pret sur http://localhost:8080");
resolve(serverInstance);
resolve(serverInstance); // succès
});
serverInstance.on("error", (err) => {
logger.error("Erreur serveur Express :", err);
@@ -77,17 +86,18 @@ function startServer() {
});
}
// Gestion propre de la fin de session à la fermeture du process
// gestion de la fermeture propre du process
// process.on("exit", () => {
// logSessionEnd();
// });
process.on("SIGINT", () => {
process.on("SIGINT", () => { // Ctrl+C
logSessionEnd();
process.exit();
});
process.on("SIGTERM", () => {
process.on("SIGTERM", () => { // kill
logSessionEnd();
process.exit();
});
// on exporte la fonction startServer pour que dautres modules puissent la lancer
module.exports = { startServer };