Push avant Clean
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 88 KiB |
165
OLD CODE/main.js
@@ -1,165 +0,0 @@
|
||||
/*
|
||||
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 { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
|
||||
app.disableHardwareAcceleration(); // Désactive l'accélération GPU pour éviter les erreurs GPU sur certains systèmes
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
||||
|
||||
let mainWindow;
|
||||
// Utilise le vrai dossier de logs défini dans logger.js
|
||||
const logsFolderPath = logDir;
|
||||
app.setAppUserModelId("com.masteracnolo.freedomloader"); /* Pour la notif */
|
||||
/*
|
||||
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() {
|
||||
logger.info("Création de la fenêtre...");
|
||||
|
||||
if (mainWindow) {
|
||||
logger.warn("La fenêtre existe déjà, pas de nouvelle création");
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
title: "Freedom Loader 1.2.4", // A Changer à chaque Version
|
||||
width: 750,
|
||||
height: 800,
|
||||
minWidth: 750,
|
||||
minHeight: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, "preload.js"), // Chargement du preload script sécurisé
|
||||
},
|
||||
// titleBarStyle: 'hidden', // Option possible pour une barre de titre personnalisée
|
||||
});
|
||||
|
||||
try {
|
||||
await mainWindow.loadURL("http://localhost:8080");
|
||||
logger.info("Fenêtre chargée");
|
||||
} catch (err) {
|
||||
logger.error("Erreur chargement fenêtre:", err);
|
||||
}
|
||||
|
||||
// Événement déclenché à la fermeture de la fenêtre principale
|
||||
mainWindow.on("closed", () => {
|
||||
logger.info("Fenêtre principale fermée");
|
||||
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 () => {
|
||||
logSessionStart();
|
||||
logger.info("App prête, démarrage du serveur Express...");
|
||||
const expressServer = require("./server/server.js");
|
||||
try {
|
||||
await expressServer.startServer();
|
||||
logger.info("Serveur Express démarré");
|
||||
await createWindow();
|
||||
|
||||
// Ajout du menu personnalisé
|
||||
const menuTemplate = [
|
||||
// ... Menu standard Electron ...
|
||||
{
|
||||
label: "Logs",
|
||||
submenu: [
|
||||
{
|
||||
label: "Ouvrir les logs",
|
||||
click: () => {
|
||||
shell.openPath(logsFolderPath);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Fusionner avec le menu par défaut
|
||||
const defaultMenu = Menu.getApplicationMenu();
|
||||
let template = menuTemplate;
|
||||
if (defaultMenu) {
|
||||
template = [...Menu.getApplicationMenu().items.map(item => item), ...menuTemplate];
|
||||
}
|
||||
const finalMenu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(finalMenu);
|
||||
|
||||
} catch (error) {
|
||||
logger.error("Erreur serveur ou fenêtre :", error);
|
||||
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", () => {
|
||||
logger.info("Toutes fenêtres fermées, quitte l'app");
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
|
||||
/*
|
||||
Avant de quitter l'application, on log la fin de session pour traçabilité.
|
||||
*/
|
||||
app.on("before-quit", () => {
|
||||
logSessionEnd();
|
||||
});
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 97 KiB |
@@ -1,3 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 968 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zm2 226.3c37.1 22.4 62 63.1 62 109.7s-24.9 87.3-62 109.7c-7.6 4.6-17.4 2.1-22-5.4s-2.1-17.4 5.4-22C269.4 401.5 288 370.9 288 336s-18.6-65.5-46.5-82.3c-7.6-4.6-10-14.4-5.4-22s14.4-10 22-5.4zm-91.9 30.9c6 2.5 9.9 8.3 9.9 14.8l0 128c0 6.5-3.9 12.3-9.9 14.8s-12.9 1.1-17.4-3.5L113.4 376 80 376c-8.8 0-16-7.2-16-16l0-48c0-8.8 7.2-16 16-16l33.4 0 35.3-35.3c4.6-4.6 11.5-5.9 17.4-3.5zm51 34.9c6.6-5.9 16.7-5.3 22.6 1.3C249.8 304.6 256 319.6 256 336s-6.2 31.4-16.3 42.7c-5.9 6.6-16 7.1-22.6 1.3s-7.1-16-1.3-22.6c5.1-5.7 8.1-13.1 8.1-21.3s-3.1-15.7-8.1-21.3c-5.9-6.6-5.3-16.7 1.3-22.6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 955 B |
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
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(){
|
||||
const title = document.getElementById("title");
|
||||
title.innerHTML = "Chirac Loader";
|
||||
|
||||
const subtitle = document.getElementById("subtitle");
|
||||
subtitle.innerHTML = "J'aime les pommes";
|
||||
subtitle.style.color= "black";
|
||||
|
||||
document.body.style.backgroundImage = "url('assets/images/goat2.webp')";
|
||||
document.body.style.backgroundSize = "cover";
|
||||
document.body.style.backgroundPosition = "center";
|
||||
document.body.style.backgroundAttachment = "fixed";
|
||||
|
||||
console.log("Je serai le président de tous les français");
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
// Liste des thèmes disponibles (clé = nom, valeur = label affiché)
|
||||
const themes = {
|
||||
default: "Default",
|
||||
dark: "Sombre",
|
||||
light: "Clair",
|
||||
nf : "NF",
|
||||
chirac: "Chirac",
|
||||
fanatic: "Fanatic",
|
||||
cyberpunk: "Cyberpunk",
|
||||
spicy: "Spicy",
|
||||
vilbrequin: "Vilbrequin"
|
||||
};
|
||||
|
||||
const themeSubtitles = {
|
||||
default: "Because why not?",
|
||||
dark: "Darkness is my ally",
|
||||
light: "Qui aime ce thème ?",
|
||||
nf : "You call it music, i call it my Therapist",
|
||||
chirac: "J'aime les pommes",
|
||||
fanatic: "Always Fnatic !",
|
||||
cyberpunk: "Wake up, choom. We’ve got a city to burn.",
|
||||
spicy: "The Spiciest One",
|
||||
vilbrequin: "Rend l'argent"
|
||||
};
|
||||
|
||||
const themeSelect = document.getElementById("themeSelect");
|
||||
|
||||
// Remplir le select avec les options
|
||||
function populateThemeSelect() {
|
||||
for (const [key, label] of Object.entries(themes)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = key;
|
||||
option.textContent = label;
|
||||
themeSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
// Appliquer le thème sur le body et changer le subtitle
|
||||
function applyTheme(themeName) {
|
||||
// Supprime les classes thème précédentes
|
||||
document.body.classList.remove(...Object.keys(themes));
|
||||
// Ajoute la classe du thème actuel
|
||||
document.body.classList.add(themeName);
|
||||
|
||||
// Modifier le subtitle
|
||||
const subtitle = document.getElementById("subtitle");
|
||||
if (subtitle && themeSubtitles[themeName]) {
|
||||
subtitle.textContent = themeSubtitles[themeName];
|
||||
}
|
||||
|
||||
// Optionnel : animation spéciale Chirac
|
||||
if (themeName === "chirac") {
|
||||
requestAnimationFrame(() => {
|
||||
document.body.classList.add("theme-active");
|
||||
});
|
||||
} else {
|
||||
document.body.classList.remove("theme-active");
|
||||
}
|
||||
}
|
||||
|
||||
// Sauvegarder le thème en localStorage
|
||||
function saveTheme(themeName) {
|
||||
localStorage.setItem("selectedTheme", themeName);
|
||||
}
|
||||
|
||||
// Charger le thème au démarrage
|
||||
function loadTheme() {
|
||||
const savedTheme = localStorage.getItem("selectedTheme");
|
||||
if (savedTheme && themes[savedTheme]) {
|
||||
applyTheme(savedTheme);
|
||||
themeSelect.value = savedTheme;
|
||||
} else {
|
||||
applyTheme("dark");
|
||||
themeSelect.value = "dark";
|
||||
}
|
||||
}
|
||||
|
||||
// Quand l’utilisateur change le select
|
||||
themeSelect.addEventListener("change", (e) => {
|
||||
const selected = e.target.value;
|
||||
if (themes[selected]) {
|
||||
applyTheme(selected);
|
||||
saveTheme(selected);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialisation
|
||||
populateThemeSelect();
|
||||
loadTheme();
|
||||
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
// On attend que le DOM soit complètement chargé
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Récupération des éléments du DOM pour interaction
|
||||
const urlInput = document.getElementById("UrlInput");
|
||||
const infoDiv = document.getElementById("videoInfo");
|
||||
|
||||
// Stocke la dernière URL fetchée pour éviter les requêtes répétées inutiles
|
||||
let lastFetchedUrl = "";
|
||||
|
||||
// Active les logs DEBUG pour le développement
|
||||
const DEBUG = true;
|
||||
|
||||
// Écoute les changements dans l'input URL
|
||||
urlInput.addEventListener("input", async () => {
|
||||
const url = urlInput.value.trim();
|
||||
|
||||
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 (DEBUG) console.log("[DEBUG] URL vide ou trop courte, reset affichage.");
|
||||
infoDiv.innerHTML = "";
|
||||
infoDiv.classList.remove("visible");
|
||||
lastFetchedUrl = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Si l'URL est identique à la dernière requêtée, on ne refait pas la requête
|
||||
if (url === lastFetchedUrl) {
|
||||
if (DEBUG) console.log("[DEBUG] Même URL que précédemment, pas de fetch.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mémorise l'URL actuelle comme dernière URL traitée
|
||||
lastFetchedUrl = url;
|
||||
|
||||
try {
|
||||
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", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ url }),
|
||||
});
|
||||
|
||||
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) {
|
||||
console.error(`[ERROR] Réponse HTTP ${res.status}: ${res.statusText}`);
|
||||
infoDiv.innerHTML = "❌ Impossible de récupérer les infos (erreur serveur).";
|
||||
infoDiv.classList.remove("visible");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse la réponse JSON contenant les infos vidéo
|
||||
const data = await res.json();
|
||||
if (DEBUG) console.log("[DEBUG] Données reçues:", data);
|
||||
|
||||
// Vérification simple que les données attendues sont présentes
|
||||
if (!data || !data.title || !data.thumbnail) {
|
||||
console.warn("[WARNING] Données incomplètes", data);
|
||||
infoDiv.innerHTML = "❌ Données incomplètes reçues.";
|
||||
infoDiv.classList.remove("visible");
|
||||
return;
|
||||
}
|
||||
|
||||
// Formatage de la durée en minutes et secondes
|
||||
const minutes = Math.floor(data.duration / 60);
|
||||
const seconds = data.duration % 60;
|
||||
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 readableDate = date
|
||||
? `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}`
|
||||
: "Inconnue";
|
||||
|
||||
// Conversion taille approximative en Mo
|
||||
const sizeMB = data.filesize_approx
|
||||
? (data.filesize_approx / (1024 * 1024)).toFixed(2) + " Mo"
|
||||
: "Inconnue";
|
||||
|
||||
// Gestion des catégories, affichage par défaut si absentes
|
||||
const categories = data.categories
|
||||
? data.categories.join(", ")
|
||||
: "Non spécifiées";
|
||||
|
||||
// Construction du HTML pour afficher les informations de la vidéo
|
||||
infoDiv.innerHTML = `
|
||||
<h3>${data.title}</h3>
|
||||
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
||||
<ul>
|
||||
<li><strong>Durée :</strong> ${durationStr}</li>
|
||||
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
|
||||
<li><strong>Date d’upload :</strong> ${readableDate}</li>
|
||||
<li><strong>Vues :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
|
||||
<li><strong>Likes :</strong> ${data.like_count?.toLocaleString() || "?"}</li>
|
||||
<li><strong>URL :</strong> <a href="${data.webpage_url}" target="_blank" rel="noopener noreferrer">${data.webpage_url}</a></li>
|
||||
<li><strong>Channel :</strong> <a href="${data.channel_url}" target="_blank" rel="noopener noreferrer">${data.channel_url}</a></li>
|
||||
<li><strong>Taille estimée :</strong> ${sizeMB}</li>
|
||||
<li><strong>Catégories :</strong> ${categories}</li>
|
||||
</ul>
|
||||
`;
|
||||
|
||||
// Affiche la div contenant les infos
|
||||
infoDiv.classList.add("visible");
|
||||
|
||||
if (DEBUG) console.log("[DEBUG] Info affichée avec succès.");
|
||||
} 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);
|
||||
infoDiv.innerHTML = "❌ Erreur réseau ou JSON.";
|
||||
infoDiv.classList.remove("visible");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
.container, .infos-container {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* height: 10000px; */
|
||||
padding: 1em;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#downloadStatus {
|
||||
|
||||
margin-top: 1em;
|
||||
min-height: 1.5em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: var(--download-status-color);
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
body.cyberpunk {
|
||||
/* Couleurs générales */
|
||||
--background-color: #0d0d0d; /* noir profond */
|
||||
--default-text-color: #e0e0e0; /* texte gris clair */
|
||||
--subtitle-color: #ffea00; /* turquoise néon pour les sous-titres */
|
||||
--mp3-text-color: #ffea00; /* jaune vif pour la checkbox */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #141414; /* fond formulaire sombre */
|
||||
--form-input-bg-color: #1a1a1a; /* inputs sombres */
|
||||
--form-input-border-color: #333333; /* bordure sobre */
|
||||
--form-input-border-focus-color: #ffea00; /* focus jaune néon */
|
||||
--form-input-text-color: #e0e0e0; /* texte clair */
|
||||
--form-input-placeholder-color: #777777; /* placeholder plus doux */
|
||||
--form-button-bg-color: #ffea00; /* bouton jaune */
|
||||
--form-button-text-color: #0d0d0d; /* texte bouton noir */
|
||||
--form-button-bg-hover-color: #d4c000; /* hover jaune plus foncé */
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #333333; /* fond checkbox sobre */
|
||||
--checkbox-bg-checked: #ffea00; /* coché en jaune vif */
|
||||
--checkbox-checkmark-border-color: #0d0d0d; /* checkmark noir */
|
||||
--checkbox-pulse-color: rgba(255, 234, 0, 0.5); /* pulse néon jaune */
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #fffbd1; /* turquoise néon */
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #1a1a1a; /* fond infos */
|
||||
--video-info-text-color: #e0e0e0; /* texte clair */
|
||||
--video-info-heading-color: #ffea00; /* titres turquoise néon */
|
||||
--video-info-list-color: #cccccc; /* texte liste gris clair */
|
||||
--video-info-list-strong-color: #ffffff; /* texte important blanc */
|
||||
--video-info-link-color: #ffea00; /* liens jaune néon */
|
||||
--video-info-link-hover-color: #d4c000; /* liens hover plus foncé */
|
||||
}
|
||||
|
||||
body.cyberpunk {
|
||||
background-image: url('../../assets/images/Cyberpunk.webp');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
body.fanatic{
|
||||
/* Couleurs générales */
|
||||
--background-color: #121212; /* fond bleu nuit/noir */
|
||||
--default-text-color: #eee; /* texte clair principal */
|
||||
--subtitle-color: #FF5900; /* sous-titres, accent orange vif */
|
||||
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #1e1e1e; /* fond formulaire sombre */
|
||||
--form-input-bg-color: #2b2b2b; /* fond inputs sombres */
|
||||
--form-input-border-color: #333333; /* bordure inputs */
|
||||
--form-input-border-focus-color: #FF5900; /* bordure focus orange */
|
||||
--form-input-text-color: #eeeeee; /* texte inputs clair */
|
||||
--form-input-placeholder-color: #666666; /* placeholder gris foncé */
|
||||
--form-button-bg-color: #FF5900; /* bouton orange */
|
||||
--form-button-text-color: #121212; /* texte bouton sombre */
|
||||
--form-button-bg-hover-color: #e65500; /* bouton hover orange foncé */
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #555555; /* gris moyen */
|
||||
--checkbox-bg-checked: #ff6600; /* orange vif */
|
||||
--checkbox-checkmark-border-color: #eee; /* blanc cassé */
|
||||
--checkbox-pulse-color: rgba(255, 102, 0, 0.5); /* pulse orange translucide */
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #FF5900; /* orange */
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #222222; /* fond infos sombre */
|
||||
--video-info-text-color: #eee; /* texte clair */
|
||||
--video-info-heading-color: #FF5900; /* titres orange */
|
||||
--video-info-list-color: #ccc; /* texte liste gris clair */
|
||||
--video-info-list-strong-color: #fff; /* texte important blanc */
|
||||
--video-info-link-color: #FF5900; /* liens orange */
|
||||
--video-info-link-hover-color: #e65500; /* liens hover orange foncé */
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
body.light {
|
||||
/* Couleurs générales */
|
||||
--background-color: #dadada; /* fond clair */
|
||||
--default-text-color: #222222; /* texte sombre */
|
||||
--subtitle-color: #007bff; /* bleu accent */
|
||||
--mp3-text-color: #000000; /* texte checkbox */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #ffffff; /* fond formulaire blanc */
|
||||
--form-input-bg-color: #fafafa; /* fond inputs */
|
||||
--form-input-border-color: #cccccc; /* bordure inputs */
|
||||
--form-input-border-focus-color: #007bff; /* focus bleu */
|
||||
--form-input-text-color: #333333; /* texte inputs */
|
||||
--form-input-placeholder-color: #888888; /* placeholder */
|
||||
--form-button-bg-color: #007bff; /* bouton bleu */
|
||||
--form-button-text-color: #ffffff; /* texte bouton */
|
||||
--form-button-bg-hover-color: #0056b3; /* hover bleu foncé */
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
--checkbox-bg-checked: #007bff;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(0, 123, 255, 0.4);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #007bff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #ffffff;
|
||||
--video-info-text-color: #222222;
|
||||
--video-info-heading-color: #007bff;
|
||||
--video-info-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
body.nf {
|
||||
/* Couleurs générales */
|
||||
--background-color: #1a1a1a; /* fond sombre neutre */
|
||||
--default-text-color: #e6dfd5; /* texte beige clair */
|
||||
--subtitle-color: #f5f0e6; /* beige très clair (titres) */
|
||||
--mp3-text-color: #d8cfc2; /* texte checkbox */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #2a2a2a; /* fond formulaire gris sombre */
|
||||
--form-input-bg-color: #222222; /* fond inputs sombre */
|
||||
--form-input-border-color: #4a4036; /* bordure beige/brun */
|
||||
--form-input-border-focus-color: #cbb89d; /* beige doré en focus */
|
||||
--form-input-text-color: #f0e8dd; /* texte clair/beige */
|
||||
--form-input-placeholder-color: #9a8f80; /* placeholder beige/gris */
|
||||
--form-button-bg-color: #cbb89d; /* bouton beige doré */
|
||||
--form-button-text-color: #1a1a1a; /* texte sombre */
|
||||
--form-button-bg-hover-color: #b7a588; /* hover beige plus foncé */
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #4a4036; /* brun/gris */
|
||||
--checkbox-bg-checked: #cbb89d; /* beige doré */
|
||||
--checkbox-checkmark-border-color: #1a1a1a; /* noir profond */
|
||||
--checkbox-pulse-color: rgba(203,184,157,0.3); /* halo beige doux */
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #cbb89d; /* beige doré */
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #2a2a2a; /* fond sombre */
|
||||
--video-info-text-color: #e6dfd5; /* texte beige clair */
|
||||
--video-info-heading-color: #f5f0e6; /* titres beige clair */
|
||||
--video-info-list-color: #d8cfc2; /* texte liste */
|
||||
--video-info-list-strong-color: #ffffff; /* texte fort blanc cassé */
|
||||
--video-info-link-color: #cbb89d; /* liens beige doré */
|
||||
--video-info-link-hover-color: #b7a588; /* hover beige plus foncé */
|
||||
}
|
||||
|
||||
/* Fond inspiré de ton image beige */
|
||||
body.nf {
|
||||
background-image: url('../../assets/images/NF.jpg'); /* mets ton image ici */
|
||||
background-size: cover;
|
||||
background-position: 70% center;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
body.nf .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
body.spicy {
|
||||
/* Couleurs générales */
|
||||
--background-color: #121212; /* fond noir profond */
|
||||
--default-text-color: #581010; /* texte clair */
|
||||
--subtitle-color: #3d0909; /* accent rouge piment */
|
||||
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #1a1a1a; /* fond formulaire gris très foncé */
|
||||
--form-input-bg-color: #2b2b2b; /* fond inputs gris foncé */
|
||||
--form-input-border-color: #444444; /* bordure gris */
|
||||
--form-input-border-focus-color: #ff1a1a; /* focus rouge piment */
|
||||
--form-input-text-color: #eeeeee; /* texte inputs */
|
||||
--form-input-placeholder-color: #888888; /* placeholder gris moyen */
|
||||
--form-button-bg-color: #ff1a1a; /* bouton rouge vif */
|
||||
--form-button-text-color: #ffffff; /* texte bouton blanc */
|
||||
--form-button-bg-hover-color: #cc0000; /* hover rouge foncé */
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #555555; /* gris moyen */
|
||||
--checkbox-bg-checked: #ff1a1a; /* rouge piment */
|
||||
--checkbox-checkmark-border-color: #f5f5f5; /* blanc cassé */
|
||||
--checkbox-pulse-color: rgba(255, 26, 26, 0.5); /* pulse rouge translucide */
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #ffffff; /* rouge vif */
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #1a1a1a; /* fond infos gris foncé */
|
||||
--video-info-text-color: #f5f5f5; /* texte clair */
|
||||
--video-info-heading-color: #ff1a1a; /* titres rouges */
|
||||
--video-info-list-color: #cccccc; /* texte liste gris clair */
|
||||
--video-info-list-strong-color: #ffffff; /* texte important blanc */
|
||||
--video-info-link-color: #ff1a1a; /* liens rouges */
|
||||
--video-info-link-hover-color: #cc0000; /* liens hover rouge foncé */
|
||||
}
|
||||
|
||||
/* Optionnel: background piments */
|
||||
body.spicy {
|
||||
background-image: url('../../assets/images/spicy.webp');
|
||||
background-size:contain;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
body.vilbrequin {
|
||||
/* Couleurs générales */
|
||||
--background-color: #0a1f3d; /* bleu Vilbrequin profond */
|
||||
--default-text-color: #535353; /* texte clair */
|
||||
--subtitle-color: #001696; /* bleu clair accent (turbo vibes) */
|
||||
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #12284f; /* fond formulaire bleu foncé */
|
||||
--form-input-bg-color: #1c3760; /* fond inputs bleu + clair */
|
||||
--form-input-border-color: #2a4b80; /* bordure bleue */
|
||||
--form-input-border-focus-color: #00bfff; /* focus bleu clair */
|
||||
--form-input-text-color: #f0f0f0; /* texte inputs clair */
|
||||
--form-input-placeholder-color: #b0c4de; /* placeholder gris bleuté */
|
||||
--form-button-bg-color: #ffd700; /* bouton jaune turbo */
|
||||
--form-button-text-color: #0a1f3d; /* texte bouton sombre */
|
||||
--form-button-bg-hover-color: #e6c200; /* hover jaune foncé */
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #2a4b80; /* bleu moyen */
|
||||
--checkbox-bg-checked: #00bfff; /* bleu clair accent */
|
||||
--checkbox-checkmark-border-color: #f0f0f0; /* blanc cassé */
|
||||
--checkbox-pulse-color: rgba(0,191,255,0.5); /* pulse bleu clair translucide */
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #ffd700; /* jaune turbo */
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #12284f; /* fond infos bleu foncé */
|
||||
--video-info-text-color: #f0f0f0; /* texte clair */
|
||||
--video-info-heading-color: #00bfff; /* titres bleu clair */
|
||||
--video-info-list-color: #dcdcdc; /* texte liste gris clair */
|
||||
--video-info-list-strong-color: #ffffff; /* texte important blanc */
|
||||
--video-info-link-color: #00bfff; /* liens bleu clair */
|
||||
--video-info-link-hover-color: #009acd; /* liens hover bleu plus foncé */
|
||||
}
|
||||
|
||||
/* Optionnel: motif turbo ou jantes */
|
||||
body.vilbrequin {
|
||||
background-image: url('../../assets/images/Vilbrequin.webp'); /* adapte ton image */
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
body.vilbrequin .download-path{
|
||||
color: white;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
:root {
|
||||
/* Couleurs générales */
|
||||
--background-color: #001224; /* body background bleu nuit */
|
||||
--default-text-color: #eee; /* texte clair principal */
|
||||
--subtitle-color: #007bff; /* sous-titres, header p */
|
||||
--mp3-text-color: #000000; /* couleur texte checkbox inline */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #ffffff; /* fond du formulaire */
|
||||
--form-input-bg-color: #f9f9f9; /* fond inputs */
|
||||
--form-input-border-color: #cccccc; /* bordure inputs */
|
||||
--form-input-border-focus-color: #0056b3; /* bordure focus */
|
||||
--form-input-text-color: #333333; /* texte inputs */
|
||||
--form-input-placeholder-color: #aaaaaa; /* placeholder */
|
||||
--form-button-bg-color: #007bff; /* bouton bg */
|
||||
--form-button-text-color: #ffffff; /* texte bouton */
|
||||
--form-button-bg-hover-color: #0056b3; /* bouton hover */
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
--checkbox-bg-checked: #3B82F6;
|
||||
--checkbox-checkmark-border-color: #E0E0E2;
|
||||
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #007bff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #f0f0f0;
|
||||
--video-info-text-color: #222222;
|
||||
--video-info-heading-color: #007bff;
|
||||
--video-info-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
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 { createLogger, format, transports } = require("winston");
|
||||
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 os = require("os"); // Pour gérer les chemins aussi
|
||||
|
||||
// Définition du dossier où seront stockés les logs
|
||||
// const logDir = path.join(__dirname, "../logs"); Désactivé parce que cela requiert d'exec en admin mode. Donc pas pratique pour les gens
|
||||
|
||||
let logDir;
|
||||
if (process.platform === "win32") {
|
||||
logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
|
||||
} else {
|
||||
logDir = path.join(os.homedir(), ".freedomloader", "logs");
|
||||
}
|
||||
|
||||
// Création du dossier logs s’il n’existe pas encore
|
||||
//if (!fs.existsSync(logDir)) fs.mkdirSync(logDir); OBSOLETE
|
||||
|
||||
fs.mkdirSync(logDir, { recursive: true }); // Evite les erreurs
|
||||
|
||||
|
||||
// Fonction utilitaire pour générer une ligne indiquant le début d’une session de logs
|
||||
function getSessionStartLine() {
|
||||
const now = new Date().toISOString();
|
||||
return `--- Démarrage de la session : ${now} ---`;
|
||||
}
|
||||
|
||||
// Fonction utilitaire pour générer une ligne indiquant la fin d’une session de logs
|
||||
function getSessionEndLine() {
|
||||
const now = new Date().toISOString();
|
||||
return `--- Fin de la session : ${now} ---`;
|
||||
}
|
||||
|
||||
// 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({
|
||||
level: "info",
|
||||
format: format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) =>
|
||||
`${timestamp} | ${level.toUpperCase()} | ${message}`
|
||||
)
|
||||
),
|
||||
transports: [
|
||||
// Rotation quotidienne des fichiers de logs
|
||||
new DailyRotateFile({
|
||||
dirname: logDir, // dossier cible pour les logs
|
||||
filename: "LOGS-%DATE%.log", // nom du fichier par date
|
||||
datePattern: "YYYY-MM-DD", // pattern de date dans le nom
|
||||
zippedArchive: false, // ne pas compresser les archives
|
||||
maxFiles: "7d", // conserve 14 jours d’historique
|
||||
format: format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) =>
|
||||
`${timestamp} | ${level.toUpperCase()} | ${message}`
|
||||
)
|
||||
),
|
||||
options: { flags: "a" }, // mode ajout à la fin (append)
|
||||
}),
|
||||
// Affichage des logs en console avec coloration et format simple
|
||||
new transports.Console({
|
||||
format: format.combine(
|
||||
format.colorize(),
|
||||
format.timestamp({ format: "HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) =>
|
||||
`${timestamp} | ${level} | ${message}`
|
||||
)
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Helper pour marquer clairement le début de session dans les logs
|
||||
function logSessionStart() {
|
||||
logger.info(getSessionStartLine());
|
||||
}
|
||||
|
||||
// Helper pour marquer la fin de session dans les logs
|
||||
function logSessionEnd() {
|
||||
logger.info(getSessionEndLine());
|
||||
}
|
||||
|
||||
// Export du logger principal et des helpers pour usage dans d’autres modules
|
||||
module.exports = {
|
||||
logger,
|
||||
logSessionStart,
|
||||
logSessionEnd,
|
||||
logDir,
|
||||
};
|
||||
@@ -1,205 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { execFile } = require("child_process");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { Notification } = require("electron");
|
||||
const logger = require("../logger").logger;
|
||||
|
||||
// Path vers le fichier exécutable yt-dlp (outil tiers pour le téléchargement)
|
||||
// const ytDlpPath = path.join(process.resourcesPath, '../../yt-dlp.exe');
|
||||
const ytDlpPath = path.join(process.resourcesPath, 'yt-dlp.exe');
|
||||
|
||||
execFile(ytDlpPath, ["-U"], (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
logger.error("Erreur update yt-dlp:", err);
|
||||
return;
|
||||
}
|
||||
logger.info(`Update yt-dlp : ${stdout}`);
|
||||
});
|
||||
|
||||
router.post("/", (req, res) => {
|
||||
try {
|
||||
// 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 = {
|
||||
url: req.body.url,
|
||||
audioOnly: req.body.audioOnly === "1",
|
||||
quality: req.body.quality || "best",
|
||||
};
|
||||
|
||||
// Validation simple : si l'URL est manquante, on rejette la requête
|
||||
if (!options.url) {
|
||||
logger.warn("Requête POST /download sans URL");
|
||||
return res.status(400).send("❌ URL manquante !");
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// On normalise le chemin pour éviter des erreurs liées aux séparateurs ou chemins relatifs
|
||||
requestedOutputFolder = path.normalize(requestedOutputFolder);
|
||||
|
||||
/*
|
||||
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é.");
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
|
||||
// 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", // pas de reprise, c'est un choix que j'ai fait voila
|
||||
//"--restrict-filenames", // noms de fichiers sans caractères spéciaux (Cyrilique, accents, etc.)
|
||||
"--no-overwrites", // évite d'écraser un fichier existant
|
||||
"--embed-thumbnail", // ajoute la pochette
|
||||
"--add-metadata", // ajoute les tags (titre, artiste, etc.)
|
||||
"--concurrent-fragments", "8",// accélère le téléchargement
|
||||
"--retries", "10", // réessaie jusqu'à 10 fois en cas d'erreur
|
||||
"--fragment-retries", "10" , // réessaie aussi 10 fois chaque fragment
|
||||
"--ffmpeg-location", path.join(process.resourcesPath, "ffmpeg.exe")
|
||||
];
|
||||
|
||||
// Si l'option audioOnly est activée, on ajoute les flags pour extraction audio en mp3
|
||||
if (options.audioOnly) {
|
||||
args.push(
|
||||
"-f",
|
||||
"bestaudio",
|
||||
"--extract-audio",
|
||||
"--audio-format",
|
||||
"mp3",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Correspondance entre la qualité choisie et le format à demander à yt-dlp
|
||||
const qualityMap = {
|
||||
best: "bestvideo+bestaudio/best",
|
||||
medium: "bestvideo[height<=720]+bestaudio/best[height<=720]",
|
||||
worst: "worstvideo+worstaudio/worst",
|
||||
1080: "bestvideo[height<=1080]+bestaudio/best[height<=1080]",
|
||||
720: "bestvideo[height<=720]+bestaudio/best[height<=720]",
|
||||
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";
|
||||
args.push("-f", format);
|
||||
|
||||
// Ajout du template de sortie et de l'URL à télécharger
|
||||
args.push("-o", outputTemplate);
|
||||
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(`Commande yt-dlp : ${ytDlpPath} ${args.join(" ")}`);
|
||||
|
||||
// Lancement du processus yt-dlp avec les arguments définis
|
||||
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) => {
|
||||
data.toString().split("\n").forEach(line => {
|
||||
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) => {
|
||||
data.toString().split("\n").forEach(line => {
|
||||
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) => {
|
||||
logger.error(`Erreur lancement yt-dlp : ${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) => {
|
||||
logger.info(`yt-dlp terminé avec code de sortie : ${code}`);
|
||||
if (code === 0) {
|
||||
|
||||
const iconPath = path.join(process.resourcesPath, "confirm-icon.png"); // la sécurité pour toutes les machines
|
||||
|
||||
console.log("Icon path pour la notif :", iconPath);
|
||||
|
||||
const notif = new Notification({
|
||||
title: "Freedom Loader",
|
||||
body: "Ton téléchargement est terminé, clique ici pour l'ouvrir.",
|
||||
icon: iconPath,
|
||||
});
|
||||
|
||||
notif.on("click", () => {
|
||||
console.log("Notification cliquée !");
|
||||
//Pour pouvoir ouvrir le dossier de la vidéo
|
||||
const { shell } = require("electron");
|
||||
shell.openPath(requestedOutputFolder);
|
||||
});
|
||||
|
||||
notif.show();
|
||||
res.send("✅ Téléchargement terminé !");
|
||||
} else {
|
||||
res.status(500).send(`❌ yt-dlp a échoué avec le code : ${code}`);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
// Capture toute autre erreur serveur non prévue, log et réponse 500
|
||||
logger.error(`Erreur serveur dans /download : ${err.message}`);
|
||||
res.status(500).send(`Erreur serveur : ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
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 express = require("express");
|
||||
const router = express.Router();
|
||||
const { execFile } = require("child_process");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { logger } = require("../logger"); // On récupère ton logger Winston
|
||||
|
||||
// Path absolu vers l'exécutable yt-dlp
|
||||
const ytDlpPath = path.join(__dirname, '../../ressources/yt-dlp.exe');
|
||||
|
||||
// Vérification que yt-dlp.exe existe bien au lancement du module
|
||||
if (!fs.existsSync(ytDlpPath)) {
|
||||
logger.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) => {
|
||||
const url = req.body.url;
|
||||
|
||||
// Validation basique : on refuse la requête sans URL
|
||||
if (!url) {
|
||||
logger.warn("Requête metadata sans URL");
|
||||
return res.status(400).send("❌ URL manquante");
|
||||
}
|
||||
|
||||
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(
|
||||
ytDlpPath,
|
||||
["--dump-json", url],
|
||||
{ timeout: 10_000 }, // Timeout fixé à 10 secondes pour éviter blocage
|
||||
(error, stdout, stderr) => {
|
||||
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.debug(`stderr: ${stderr}`);
|
||||
return res.status(500).send("❌ Impossible de récupérer les infos.");
|
||||
}
|
||||
|
||||
try {
|
||||
// yt-dlp peut retourner plusieurs JSON séparés par des sauts de ligne
|
||||
// On split et parse chacun proprement
|
||||
const infos = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map(line => JSON.parse(line));
|
||||
|
||||
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);
|
||||
} catch (e) {
|
||||
// Si parsing JSON échoue, on log et renvoie une erreur 500
|
||||
logger.error(`Erreur parsing JSON: ${e.message}`);
|
||||
return res.status(500).send("❌ JSON illisible.");
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,106 +0,0 @@
|
||||
/*
|
||||
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 express = require("express"); // Framework web pour créer l’API et servir les fichiers
|
||||
const { exec } = require("child_process"); // Permet d’exécuter des commandes systèmes (pas utilisé directement ici)
|
||||
const fs = require("fs"); // Module système pour gérer fichiers et dossiers
|
||||
const path = require("path"); // Gestion propre des chemins de fichiers et dossiers
|
||||
const debug = require("debug")("freedom-loader:server");
|
||||
// Module de debug coloré en console, pratique pour dev
|
||||
|
||||
const { logger, logSessionStart, logSessionEnd } = require("./logger");
|
||||
// Import du logger Winston et helpers pour marquer début/fin de session
|
||||
|
||||
const app = express(); // Création de l’instance Express, notre serveur web
|
||||
|
||||
// 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)
|
||||
const downloadsPath = path.join(process.env.USERPROFILE, "Downloads");
|
||||
const outputFolder = path.join(downloadsPath, "Freedom Loader");
|
||||
|
||||
// Création du dossier de sortie s’il n’existe pas déjà
|
||||
if (!fs.existsSync(outputFolder)) {
|
||||
try {
|
||||
fs.mkdirSync(outputFolder, { recursive: true }); // création récursive au cas où
|
||||
logger.info("Dossier Freedom Loader cree dans Telechargements.");
|
||||
} catch (err) {
|
||||
logger.error("Impossible de creer le dossier :", err);
|
||||
process.exit(1); // Arrêt du programme si dossier non créé (critique)
|
||||
}
|
||||
} else {
|
||||
logger.info("Dossier Freedom Loader deja existant.");
|
||||
}
|
||||
|
||||
// On rend ce dossier accessible globalement via app.locals pour l’utiliser dans les routes
|
||||
app.locals.outputFolder = outputFolder;
|
||||
|
||||
// Middleware pour parser le corps des requêtes POST en application/x-www-form-urlencoded
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// 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");
|
||||
debug("Serveur statique sur", staticPath);
|
||||
app.use(express.static(staticPath));
|
||||
|
||||
// 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 downloadRoute = require("./routes/download");
|
||||
debug("Routes /download et /info installees");
|
||||
app.use("/download", downloadRoute);
|
||||
app.use("/info", infoRoute);
|
||||
|
||||
// Route GET / qui sert la page principale index.html
|
||||
app.get("/", (req, res) => {
|
||||
debug("Requete GET / servie");
|
||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||
});
|
||||
|
||||
// Fonction pour démarrer le serveur Express
|
||||
// Retourne une Promise pour pouvoir await le démarrage dans d’autres modules
|
||||
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); // Serveur prêt, on résout la promesse
|
||||
});
|
||||
// Gestion des erreurs serveur lors du démarrage
|
||||
serverInstance.on("error", (err) => {
|
||||
logger.error("Erreur serveur Express :", err);
|
||||
reject(err); // Rejet de la promesse en cas d’erreur critique
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Gestion propre de la fermeture du process pour logger la fin de session
|
||||
process.on("SIGINT", () => { // Capture Ctrl+C (interruption)
|
||||
logSessionEnd();
|
||||
process.exit();
|
||||
});
|
||||
process.on("SIGTERM", () => { // Capture kill ou arrêt du process
|
||||
logSessionEnd();
|
||||
process.exit();
|
||||
});
|
||||
|
||||
// Export de la fonction startServer pour permettre son appel depuis d’autres fichiers
|
||||
module.exports = { startServer };
|
||||
|
||||
const { startRPC } = require("./discordRPC");
|
||||
startRPC();
|
||||
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
@@ -2,35 +2,16 @@ const { FusesPlugin } = require('@electron-forge/plugin-fuses');
|
||||
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
|
||||
|
||||
module.exports = {
|
||||
packagerConfig: {
|
||||
asar: true,
|
||||
},
|
||||
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
|
||||
{ name: '@electron-forge/plugin-auto-unpack-natives', config: {} },
|
||||
new FusesPlugin({
|
||||
version: FuseVersion.V1,
|
||||
[FuseV1Options.RunAsNode]: false,
|
||||
124
main.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
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 { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
||||
|
||||
app.disableHardwareAcceleration(); // safe sur GPU old / soucis Electron
|
||||
|
||||
let mainWindow;
|
||||
const logsFolderPath = logDir;
|
||||
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
||||
|
||||
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
|
||||
|
||||
// ---------- FENÊTRE ---------- //
|
||||
async function createWindow() {
|
||||
if (mainWindow) {
|
||||
logger.warn("La fenêtre existe déjà, pas de nouvelle création");
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
title: "Freedom Loader 1.3.0",
|
||||
width: 750,
|
||||
height: 800,
|
||||
minWidth: 750,
|
||||
minHeight: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await mainWindow.loadURL(`http://localhost:8787`);
|
||||
logger.info("Fenêtre chargée");
|
||||
} catch (err) {
|
||||
logger.error("Erreur chargement fenêtre:", err);
|
||||
}
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
logger.info("Fenêtre principale fermée");
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
// IPC
|
||||
ipcMain.handle("select-download-folder", async () => {
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("get-default-download-path", () => defaultDownloadPath);
|
||||
|
||||
function setupMenu() {
|
||||
const menuTemplate = [
|
||||
{
|
||||
label: "Logs",
|
||||
submenu: [
|
||||
{
|
||||
label: "Ouvrir les logs",
|
||||
click: () => shell.openPath(logsFolderPath),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const defaultMenu = Menu.getApplicationMenu();
|
||||
const mergedTemplate = defaultMenu
|
||||
? [...defaultMenu.items.map(item => item), ...menuTemplate]
|
||||
: menuTemplate;
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(mergedTemplate));
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
logSessionStart();
|
||||
logger.info("App prête, démarrage du serveur Express...");
|
||||
|
||||
const expressServer = require("./server/server.js");
|
||||
try {
|
||||
await expressServer.startServer();
|
||||
logger.info("Serveur Express démarré");
|
||||
await createWindow();
|
||||
setupMenu();
|
||||
} catch (err) {
|
||||
logger.error("Erreur serveur ou fenêtre :", err);
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
logger.info("Toutes fenêtres fermées, quitte l'app");
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
|
||||
app.on("before-quit", () => logSessionEnd());
|
||||
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 805 KiB After Width: | Height: | Size: 805 KiB |
BIN
public/assets/images/Drift.jpg
Normal file
|
After Width: | Height: | Size: 967 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -14,8 +14,6 @@
|
||||
<!--CSS-->
|
||||
<link rel="stylesheet" href="styles/styles.css">
|
||||
|
||||
<!--Icons-->
|
||||
<link rel="icon" href="assets/icons/file-audio-solid.svg" type="image/svg+xml">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
92
public/script/customthemes.js
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
// Définition des thèmes disponibles
|
||||
// Chaque thème a un label (affiché dans le select) et un subtitle (texte sous le titre)
|
||||
const themes = {
|
||||
default: { label: "Default", subtitle: "Because why not?" },
|
||||
dark: { label: "Sombre", subtitle: "Darkness is my ally" },
|
||||
light: { label: "Clair", subtitle: "Qui aime ce thème ?" },
|
||||
neon: { label: "Néon", subtitle: "How was your day ?"},
|
||||
nf: { label: "NF", subtitle: "You call it music, i call it my Therapist" },
|
||||
drift: { label: "Drift", subtitle: "Si la route t'appelle, contre appel" },
|
||||
fanatic: { label: "Fanatic", subtitle: "Always Fnatic !" },
|
||||
cyberpunk: { label: "Cyberpunk", subtitle: "Wake up, choom. We’ve got a city to burn." },
|
||||
chirac: { label: "Chirac", subtitle: "J'aime les pommes" },
|
||||
spicy: { label: "Spicy", subtitle: "The Spiciest One" },
|
||||
vilbrequin: { label: "Vilbrequin", subtitle: "Rend l'argent" }
|
||||
};
|
||||
|
||||
|
||||
const themeSelect = document.getElementById("themeSelect");
|
||||
|
||||
// Remplir le select avec les options à partir du dictionnaire
|
||||
function populateThemeSelect() {
|
||||
for (const [themeKey, themeInfo] of Object.entries(themes)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = themeKey;
|
||||
option.textContent = themeInfo.label;
|
||||
themeSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
// Appliquer un thème sur le body et mettre à jour le subtitle
|
||||
function applyTheme(themeKey) {
|
||||
// Supprimer les classes de tous les thèmes
|
||||
document.body.classList.remove(...Object.keys(themes));
|
||||
// Ajouter la classe correspondant au thème sélectionné
|
||||
document.body.classList.add(themeKey);
|
||||
|
||||
// Mettre à jour le subtitle
|
||||
const subtitleElement = document.getElementById("subtitle");
|
||||
if (subtitleElement && themes[themeKey]) {
|
||||
subtitleElement.textContent = themes[themeKey].subtitle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Sauvegarder le thème choisi dans le navigateur
|
||||
function saveTheme(themeKey) {
|
||||
localStorage.setItem("selectedTheme", themeKey);
|
||||
}
|
||||
|
||||
// Charger le thème sauvegardé au démarrage
|
||||
function loadTheme() {
|
||||
const savedTheme = localStorage.getItem("selectedTheme");
|
||||
|
||||
if (savedTheme && themes[savedTheme]) {
|
||||
applyTheme(savedTheme);
|
||||
themeSelect.value = savedTheme;
|
||||
} else {
|
||||
applyTheme("dark"); // thème par défaut
|
||||
themeSelect.value = "dark";
|
||||
}
|
||||
}
|
||||
|
||||
// Quand l'utilisateur change le thème depuis le select
|
||||
themeSelect.addEventListener("change", (event) => {
|
||||
const selectedTheme = event.target.value;
|
||||
if (themes[selectedTheme]) {
|
||||
applyTheme(selectedTheme);
|
||||
saveTheme(selectedTheme);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialisation
|
||||
populateThemeSelect();
|
||||
loadTheme();
|
||||
@@ -16,56 +16,38 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Récupération du formulaire de téléchargement
|
||||
const form = document.getElementById("downloadForm");
|
||||
|
||||
// Élément où on affichera l’état du téléchargement (en cours, erreur, succès)
|
||||
const statusDiv = document.getElementById("downloadStatus");
|
||||
|
||||
// Récupération du bouton de soumission pour pouvoir le désactiver pendant la requête
|
||||
const button = form.querySelector("button");
|
||||
|
||||
// Écouteur d’événement pour intercepter la soumission du formulaire
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault(); // Empêche le rechargement automatique de la page
|
||||
|
||||
// Désactive le bouton pour éviter les clics multiples pendant la requête
|
||||
button.disabled = true;
|
||||
|
||||
// Affiche un message d’attente pour informer l’utilisateur
|
||||
e.preventDefault();
|
||||
button.disabled = true; // Empêche les clics multiples
|
||||
statusDiv.textContent = "Téléchargement en cours...";
|
||||
|
||||
// Prépare les données du formulaire au format URL-encoded
|
||||
const formData = new FormData(form);
|
||||
const params = new URLSearchParams(formData);
|
||||
|
||||
try {
|
||||
// Envoie la requête POST vers /download avec les données du formulaire
|
||||
const res = await fetch("/download", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: params,
|
||||
});
|
||||
|
||||
// Si la réponse n’est pas un succès (code HTTP 4xx ou 5xx)
|
||||
if (!res.ok) {
|
||||
statusDiv.textContent = "❌ Erreur pendant le téléchargement.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupère le texte envoyé par le serveur (message de succès ou erreur)
|
||||
const text = await res.text();
|
||||
|
||||
// Affiche le message retourné par le serveur
|
||||
statusDiv.textContent = text;
|
||||
|
||||
} catch {
|
||||
// Gestion des erreurs réseau ou autres exceptions
|
||||
statusDiv.textContent = "❌ Une erreur s’est produite.";
|
||||
} finally {
|
||||
// Réactive le bouton pour permettre d’autres soumissions
|
||||
button.disabled = false;
|
||||
|
||||
// Efface le message d’état après 5 secondes pour garder l’interface propre
|
||||
setTimeout(() => {
|
||||
statusDiv.textContent = "";
|
||||
}, 5000);
|
||||
122
public/script/fetchinfo.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
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 formatDate(dateStr) {
|
||||
if (!dateStr || dateStr.length !== 8) return "Inconnue";
|
||||
return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`;
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
return bytes ? (bytes / (1024*1024)).toFixed(2) + " Mo" : "Inconnue";
|
||||
}
|
||||
|
||||
async function fetchVideoInfo(url) {
|
||||
try {
|
||||
const res = await fetch("/info", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ url }),
|
||||
});
|
||||
|
||||
if (!res.ok) return { error: `Erreur serveur: ${res.status}` };
|
||||
|
||||
const data = await res.json();
|
||||
if (!data) return { error: "Données manquantes" };
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
return { error: "Erreur réseau ou JSON" };
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const urlInput = document.getElementById("UrlInput");
|
||||
const infoDiv = document.getElementById("videoInfo");
|
||||
let lastFetchedUrl = "";
|
||||
|
||||
urlInput.addEventListener("input", async () => {
|
||||
const url = urlInput.value.trim();
|
||||
|
||||
if (!url || url.length < 5) {
|
||||
infoDiv.innerHTML = "";
|
||||
infoDiv.classList.remove("visible");
|
||||
lastFetchedUrl = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (url === lastFetchedUrl) return;
|
||||
lastFetchedUrl = url;
|
||||
|
||||
const data = await fetchVideoInfo(url);
|
||||
if (data.error) {
|
||||
infoDiv.innerHTML = `❌ ${data.error}`;
|
||||
infoDiv.classList.remove("visible");
|
||||
return;
|
||||
}
|
||||
|
||||
// Playlist
|
||||
if (data.type === "playlist") {
|
||||
infoDiv.innerHTML = `
|
||||
<p style="color:orange;"><strong>Playlist détectée – ${data.count} vidéos</strong></p>
|
||||
<h3>${data.title}</h3>
|
||||
<p><strong>Channel :</strong> ${data.channel || "Inconnu"}</p>
|
||||
<div id="playlistVideos"></div>
|
||||
`;
|
||||
const listDiv = document.getElementById("playlistVideos");
|
||||
data.videos.forEach(v => {
|
||||
const durationStr = `${Math.floor(v.duration/60)}m ${(v.duration%60).toString().padStart(2,"0")}s`;
|
||||
listDiv.innerHTML += `
|
||||
<div style="margin-bottom:12px;">
|
||||
<img src="${v.thumbnail}" width="160" alt="Thumbnail">
|
||||
<p><strong>${v.title}</strong></p>
|
||||
<p>Durée : ${durationStr}</p>
|
||||
<p>URL : <a href="${v.webpage_url}" target="_blank">${v.webpage_url}</a></p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
infoDiv.classList.add("visible");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Vidéo normale
|
||||
const durationStr = `${Math.floor(data.duration/60)}m ${(data.duration%60)
|
||||
.toString()
|
||||
.padStart(2,"0")}s`;
|
||||
const sizeStr = formatSize(data.filesize_approx);
|
||||
const readableDate = formatDate(data.upload_date);
|
||||
const categories = data.categories?.join(", ") || "Non spécifiées";
|
||||
|
||||
infoDiv.innerHTML = `
|
||||
<h3>${data.title}</h3>
|
||||
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
||||
<ul>
|
||||
<li><strong>Durée :</strong> ${durationStr}</li>
|
||||
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
|
||||
<li><strong>Date d’upload :</strong> ${readableDate}</li>
|
||||
<li><strong>Vues :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
|
||||
<li><strong>Likes :</strong> ${data.like_count?.toLocaleString() || "?"}</li>
|
||||
<li><strong>URL :</strong> <a href="${data.webpage_url}" target="_blank">${data.webpage_url}</a></li>
|
||||
<li><strong>Channel :</strong> <a href="${data.channel_url}" target="_blank">${data.channel_url}</a></li>
|
||||
<li><strong>Taille estimée :</strong> ${sizeStr}</li>
|
||||
<li><strong>Catégories :</strong> ${categories}</li>
|
||||
</ul>
|
||||
`;
|
||||
infoDiv.classList.add("visible");
|
||||
});
|
||||
})
|
||||
25
public/styles/layout/container.css
Normal file
@@ -0,0 +1,25 @@
|
||||
.container, .infos-container {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1em;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#downloadStatus {
|
||||
margin-top: 1em;
|
||||
min-height: 1.5em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: var(--download-status-color);
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
/* Layout */
|
||||
@import url("layout/form.css");
|
||||
@import url("layout/container.css");
|
||||
@import url("layout/ctr-infos.css");
|
||||
@import url("layout/header.css");
|
||||
@import url("layout/videoinfo.css");
|
||||
|
||||
/* Components */
|
||||
@import url("components/checkbox.css");
|
||||
@import url("components/themebutton.css");
|
||||
/* @import url("variables.css"); */
|
||||
|
||||
@import url("theme/themeimport.css");
|
||||
@import url("components/editpathbutton.css");
|
||||
|
||||
@@ -24,7 +26,7 @@ html,body{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
scroll-behavior: smooth;
|
||||
/* outline: 2px solid red; */ /*<-- Utile pour le début ça */
|
||||
/*outline: 2px solid red; /*<-- Utile pour le debug ça */
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
@@ -33,10 +35,6 @@ html,body{
|
||||
body {
|
||||
margin: 0;
|
||||
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; */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--default-text-color);
|
||||
@@ -47,7 +45,6 @@ body {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
|
||||
}
|
||||
|
||||
header p{
|
||||
@@ -1,16 +1,15 @@
|
||||
body.chirac {
|
||||
/* Background image Chirac */
|
||||
|
||||
background-image: url('../../assets/images/Chirac.png');
|
||||
/* background-position: right; */
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Couleurs drapeau français */
|
||||
--background-color: rgba(255, 255, 255, 0.85); /* blanc légèrement transparent pour lisibilité */
|
||||
--default-text-color: #002395; /* bleu foncé */
|
||||
--subtitle-color: #EF4135; /* rouge vif */
|
||||
--mp3-text-color: #002395; /* bleu */
|
||||
|
||||
--background-color: rgba(255, 255, 255, 0.85);
|
||||
--default-text-color: #002395;
|
||||
--subtitle-color: #EF4135;
|
||||
--mp3-text-color: #002395;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: rgba(255, 255, 255, 0.9);
|
||||
@@ -19,9 +18,9 @@ body.chirac {
|
||||
--form-input-border-focus-color: #EF4135;
|
||||
--form-input-text-color: #002395;
|
||||
--form-input-placeholder-color: #999999;
|
||||
--form-button-bg-color: #002395; /* bleu */
|
||||
--form-button-bg-color: #002395;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #EF4135; /* rouge */
|
||||
--form-button-bg-hover-color: #EF4135;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
@@ -40,15 +39,8 @@ body.chirac {
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #002395;
|
||||
--video-info-link-hover-color: #EF4135;
|
||||
|
||||
/* animation d'apparition */
|
||||
opacity: 0;
|
||||
transition: opacity 0.8s ease, background-color 0.8s ease;
|
||||
}
|
||||
|
||||
body.chirac.theme-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
43
public/styles/theme/cyberpunk-theme.css
Normal file
@@ -0,0 +1,43 @@
|
||||
body.cyberpunk {
|
||||
|
||||
background-image: url('../../assets/images/Cyberpunk.webp');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Couleurs générales */
|
||||
--background-color: #0d0d0d;
|
||||
--default-text-color: #e0e0e0;
|
||||
--subtitle-color: #ffea00;
|
||||
--mp3-text-color: #ffea00;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #141414;
|
||||
--form-input-bg-color: #1a1a1a;
|
||||
--form-input-border-color: #333333;
|
||||
--form-input-border-focus-color: #ffea00;
|
||||
--form-input-text-color: #e0e0e0;
|
||||
--form-input-placeholder-color: #777777;
|
||||
--form-button-bg-color: #ffea00;
|
||||
--form-button-text-color: #0d0d0d;
|
||||
--form-button-bg-hover-color: #d4c000;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #333333;
|
||||
--checkbox-bg-checked: #ffea00;
|
||||
--checkbox-checkmark-border-color: #0d0d0d;
|
||||
--checkbox-pulse-color: rgba(255, 234, 0, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #fffbd1;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #1a1a1a;
|
||||
--video-info-text-color: #e0e0e0;
|
||||
--video-info-heading-color: #ffea00;
|
||||
--video-info-list-color: #cccccc;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #ffea00;
|
||||
--video-info-link-hover-color: #d4c000;
|
||||
}
|
||||
47
public/styles/theme/drift-theme.css
Normal file
@@ -0,0 +1,47 @@
|
||||
body.drift {
|
||||
|
||||
background-image: url('../../assets/images/Drift.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Palette principale */
|
||||
--background-color: #0f1218;
|
||||
--default-text-color: #3d4b61;
|
||||
--subtitle-color: #1a1a1a;
|
||||
--mp3-text-color: #c0d4ff;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #1e222a;
|
||||
--form-input-bg-color: #13171d;
|
||||
--form-input-border-color: #3b4b6d;
|
||||
--form-input-border-focus-color: #6ea8ff;
|
||||
--form-input-text-color: #f0f4ff;
|
||||
--form-input-placeholder-color: #7a8fae;
|
||||
--form-button-bg-color: #2a2979;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #353393;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #2a2e38;
|
||||
--checkbox-bg-checked: #2a2979;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(76,108,255,0.3);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #ff8e8e;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #1e222a;
|
||||
--video-info-text-color: #e8e8f0;
|
||||
--video-info-heading-color: #9ab6ff;
|
||||
--video-info-list-color: #c0d4ff;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #4c6cff;
|
||||
--video-info-link-hover-color: #6c8dff;
|
||||
}
|
||||
|
||||
|
||||
body.drift .download-path{
|
||||
color: #ededed;
|
||||
}
|
||||
36
public/styles/theme/fanatic-theme.css
Normal file
@@ -0,0 +1,36 @@
|
||||
body.fanatic{
|
||||
/* Couleurs générales */
|
||||
--background-color: #121212;
|
||||
--default-text-color: #eee;
|
||||
--subtitle-color: #FF5900;
|
||||
--mp3-text-color: #ffffff;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #1e1e1e;
|
||||
--form-input-bg-color: #2b2b2b;
|
||||
--form-input-border-color: #333333;
|
||||
--form-input-border-focus-color: #FF5900;
|
||||
--form-input-text-color: #eeeeee;
|
||||
--form-input-placeholder-color: #666666;
|
||||
--form-button-bg-color: #FF5900;
|
||||
--form-button-text-color: #121212;
|
||||
--form-button-bg-hover-color: #e65500;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #555555;
|
||||
--checkbox-bg-checked: #ff6600;
|
||||
--checkbox-checkmark-border-color: #eee;
|
||||
--checkbox-pulse-color: rgba(255, 102, 0, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #FF5900;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #222222;
|
||||
--video-info-text-color: #eee;
|
||||
--video-info-heading-color: #FF5900;
|
||||
--video-info-list-color: #ccc;
|
||||
--video-info-list-strong-color: #fff;
|
||||
--video-info-link-color: #FF5900;
|
||||
--video-info-link-hover-color: #e65500;
|
||||
}
|
||||
36
public/styles/theme/light-theme.css
Normal file
@@ -0,0 +1,36 @@
|
||||
body.light {
|
||||
/* Couleurs générales */
|
||||
--background-color: #dadada;
|
||||
--default-text-color: #222222;
|
||||
--subtitle-color: #007bff;
|
||||
--mp3-text-color: #000000;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #ffffff;
|
||||
--form-input-bg-color: #fafafa;
|
||||
--form-input-border-color: #cccccc;
|
||||
--form-input-border-focus-color: #007bff;
|
||||
--form-input-text-color: #333333;
|
||||
--form-input-placeholder-color: #888888;
|
||||
--form-button-bg-color: #007bff;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #0056b3;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
--checkbox-bg-checked: #007bff;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(0, 123, 255, 0.4);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #007bff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #ffffff;
|
||||
--video-info-text-color: #222222;
|
||||
--video-info-heading-color: #007bff;
|
||||
--video-info-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
}
|
||||
47
public/styles/theme/neon-theme.css
Normal file
@@ -0,0 +1,47 @@
|
||||
body.neon {
|
||||
|
||||
background: (url("../../assets/images/drift.jpg"));
|
||||
background-image: linear-gradient(135deg, #0f0c29, #302b63, #24243e); /* dégradé sombre violet/bleu */
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Couleurs générales */
|
||||
--background-color: #1b1b2f;
|
||||
--default-text-color: #e0e0ff;
|
||||
--subtitle-color: #a3a3ff;
|
||||
--mp3-text-color: #c0c0ff;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #2c2c4a;
|
||||
--form-input-bg-color: #1f1f3a;
|
||||
--form-input-border-color: #5858f0;
|
||||
--form-input-border-focus-color: #a3a3ff;
|
||||
--form-input-text-color: #ffffff;
|
||||
--form-input-placeholder-color: #8888ff;
|
||||
--form-button-bg-color: #5858f0;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #7a7aff;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #302b63;
|
||||
--checkbox-bg-checked: #5858f0;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(88,88,240,0.3);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #a3a3ff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #2c2c4a;
|
||||
--video-info-text-color: #e0e0ff;
|
||||
--video-info-heading-color: #a3a3ff;
|
||||
--video-info-list-color: #c0c0ff;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #5858f0;
|
||||
--video-info-link-hover-color: #7a7aff;
|
||||
}
|
||||
|
||||
body.neon .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
46
public/styles/theme/nf-theme.css
Normal file
@@ -0,0 +1,46 @@
|
||||
body.nf {
|
||||
|
||||
background-image: url('../../assets/images/NF.jpg');
|
||||
background-size: cover;
|
||||
background-position: 70% center;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Couleurs générales */
|
||||
--background-color: #1a1a1a;
|
||||
--default-text-color: #e6dfd5;
|
||||
--subtitle-color: #f5f0e6;
|
||||
--mp3-text-color: #d8cfc2;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #2a2a2a;
|
||||
--form-input-bg-color: #222222;
|
||||
--form-input-border-color: #4a4036;
|
||||
--form-input-border-focus-color: #cbb89d;
|
||||
--form-input-text-color: #f0e8dd;
|
||||
--form-input-placeholder-color: #9a8f80;
|
||||
--form-button-bg-color: #cbb89d;
|
||||
--form-button-text-color: #1a1a1a;
|
||||
--form-button-bg-hover-color: #b7a588;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #4a4036;
|
||||
--checkbox-bg-checked: #cbb89d;
|
||||
--checkbox-checkmark-border-color: #1a1a1a;
|
||||
--checkbox-pulse-color: rgba(203,184,157,0.3);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #cbb89d;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #2a2a2a;
|
||||
--video-info-text-color: #e6dfd5;
|
||||
--video-info-heading-color: #f5f0e6;
|
||||
--video-info-list-color: #d8cfc2;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #cbb89d;
|
||||
--video-info-link-hover-color: #b7a588;
|
||||
}
|
||||
|
||||
body.nf .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
43
public/styles/theme/spicy-theme.css
Normal file
@@ -0,0 +1,43 @@
|
||||
body.spicy {
|
||||
|
||||
background-image: url('../../assets/images/spicy.webp');
|
||||
background-size:contain;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
/* Couleurs générales */
|
||||
--background-color: #121212;
|
||||
--default-text-color: #581010;
|
||||
--subtitle-color: #3d0909;
|
||||
--mp3-text-color: #ffffff;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #1a1a1a;
|
||||
--form-input-bg-color: #2b2b2b;
|
||||
--form-input-border-color: #444444;
|
||||
--form-input-border-focus-color: #ff1a1a;
|
||||
--form-input-text-color: #eeeeee;
|
||||
--form-input-placeholder-color: #888888;
|
||||
--form-button-bg-color: #ff1a1a;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #cc0000;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #555555;
|
||||
--checkbox-bg-checked: #ff1a1a;
|
||||
--checkbox-checkmark-border-color: #f5f5f5;
|
||||
--checkbox-pulse-color: rgba(255, 26, 26, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #ffffff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #1a1a1a;
|
||||
--video-info-text-color: #f5f5f5;
|
||||
--video-info-heading-color: #ff1a1a;
|
||||
--video-info-list-color: #cccccc;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #ff1a1a;
|
||||
--video-info-link-hover-color: #cc0000;
|
||||
}
|
||||
@@ -7,3 +7,5 @@
|
||||
@import url("spicy-theme.css");
|
||||
@import url("vilbrequin-theme.css");
|
||||
@import url("nf-theme.css");
|
||||
@import url("drift-theme.css");
|
||||
@import url("neon-theme.css");
|
||||
46
public/styles/theme/vilbrequin-theme.css
Normal file
@@ -0,0 +1,46 @@
|
||||
body.vilbrequin {
|
||||
|
||||
background-image: url('../../assets/images/Vilbrequin.webp');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Couleurs générales */
|
||||
--background-color: #0a1f3d;
|
||||
--default-text-color: #535353;
|
||||
--subtitle-color: #001696;
|
||||
--mp3-text-color: #ffffff;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #12284f;
|
||||
--form-input-bg-color: #1c3760;
|
||||
--form-input-border-color: #2a4b80;
|
||||
--form-input-border-focus-color: #00bfff;
|
||||
--form-input-text-color: #f0f0f0;
|
||||
--form-input-placeholder-color: #b0c4de;
|
||||
--form-button-bg-color: #ffd700;
|
||||
--form-button-text-color: #0a1f3d;
|
||||
--form-button-bg-hover-color: #e6c200;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #2a4b80;
|
||||
--checkbox-bg-checked: #00bfff;
|
||||
--checkbox-checkmark-border-color: #f0f0f0;
|
||||
--checkbox-pulse-color: rgba(0,191,255,0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #ffd700;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #12284f;
|
||||
--video-info-text-color: #f0f0f0;
|
||||
--video-info-heading-color: #00bfff;
|
||||
--video-info-list-color: #dcdcdc;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #00bfff;
|
||||
--video-info-link-hover-color: #009acd;
|
||||
}
|
||||
|
||||
body.vilbrequin .download-path{
|
||||
color: white;
|
||||
}
|
||||
36
public/styles/variables.css
Normal file
@@ -0,0 +1,36 @@
|
||||
:root {
|
||||
/* Couleurs générales */
|
||||
--background-color: #001224;
|
||||
--default-text-color: #eee;
|
||||
--subtitle-color: #007bff;
|
||||
--mp3-text-color: #000000;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #ffffff;
|
||||
--form-input-bg-color: #f9f9f9;
|
||||
--form-input-border-color: #cccccc;
|
||||
--form-input-border-focus-color: #0056b3;
|
||||
--form-input-text-color: #333333;
|
||||
--form-input-placeholder-color: #aaaaaa;
|
||||
--form-button-bg-color: #007bff;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #0056b3;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
--checkbox-bg-checked: #3B82F6;
|
||||
--checkbox-checkmark-border-color: #E0E0E2;
|
||||
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #007bff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #f0f0f0;
|
||||
--video-info-text-color: #222222;
|
||||
--video-info-heading-color: #007bff;
|
||||
--video-info-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
}
|
||||
@@ -20,22 +20,24 @@ const RPC = require("discord-rpc");
|
||||
const clientId = "1410934537051181146";
|
||||
const rpc = new RPC.Client({ transport: "ipc" });
|
||||
|
||||
let intervalId;
|
||||
|
||||
function startRPC() {
|
||||
rpc.on("ready", () => {
|
||||
console.log("✅ Connecté à Discord !");
|
||||
console.log("Connecté à Discord !");
|
||||
|
||||
const presence = {
|
||||
largeImageKey: "icon",
|
||||
smallImageKey: "acnolo_pfp",
|
||||
smallImageText: "By MasterAcnolo",
|
||||
startTimestamp: new Date(), // compteur de temps
|
||||
startTimestamp: new Date(),
|
||||
details: "github.com/MasterAcnolo/Freedom-Loader",
|
||||
};
|
||||
|
||||
rpc.setActivity(presence);
|
||||
|
||||
// Mise à jour toutes les 15 secondes si tu veux garder le compteur à jour
|
||||
setInterval(() => {
|
||||
// Met à jour la présence toutes les 15s
|
||||
intervalId = setInterval(() => {
|
||||
rpc.setActivity(presence);
|
||||
}, 15000);
|
||||
});
|
||||
@@ -43,6 +45,23 @@ function startRPC() {
|
||||
rpc.login({ clientId }).catch(err => {
|
||||
console.error("Erreur Discord RPC :", err);
|
||||
});
|
||||
|
||||
// Gestion propre de la fermeture
|
||||
const cleanExit = () => {
|
||||
if (intervalId) clearInterval(intervalId); // stop interval
|
||||
rpc.destroy(); // déconnecte proprement
|
||||
console.log("Discord RPC arrêté proprement.");
|
||||
};
|
||||
|
||||
process.on("exit", cleanExit);
|
||||
process.on("SIGINT", () => {
|
||||
cleanExit();
|
||||
process.exit();
|
||||
});
|
||||
process.on("SIGTERM", () => {
|
||||
cleanExit();
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { startRPC };
|
||||
86
server/logger.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
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 { createLogger, format, transports } = require("winston");
|
||||
const DailyRotateFile = require("winston-daily-rotate-file");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
|
||||
// Dossier de logs Windows
|
||||
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
|
||||
|
||||
// Création du dossier si nécessaire
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
|
||||
// Format commun pour tous les logs
|
||||
const logFormat = format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
||||
);
|
||||
|
||||
const consoleFormat = format.combine(
|
||||
format.colorize(),
|
||||
format.timestamp({ format: "HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level} | ${message}`)
|
||||
);
|
||||
|
||||
// Configuration du logger
|
||||
const logger = createLogger({
|
||||
level: "info",
|
||||
format: logFormat,
|
||||
transports: [
|
||||
new DailyRotateFile({
|
||||
dirname: logDir,
|
||||
filename: "LOGS-%DATE%.log",
|
||||
datePattern: "YYYY-MM-DD",
|
||||
zippedArchive: false,
|
||||
maxFiles: "7d",
|
||||
format: logFormat,
|
||||
options: { flags: "a" },
|
||||
}),
|
||||
new transports.Console({
|
||||
format: consoleFormat,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Helpers pour sessions
|
||||
function getSessionStartLine() {
|
||||
return `--- Démarrage de la session : ${new Date().toISOString()} ---`;
|
||||
}
|
||||
|
||||
function getSessionEndLine() {
|
||||
return `--- Fin de la session : ${new Date().toISOString()} ---`;
|
||||
}
|
||||
|
||||
function logSessionStart() {
|
||||
logger.info(getSessionStartLine());
|
||||
}
|
||||
|
||||
function logSessionEnd() {
|
||||
logger.info(getSessionEndLine());
|
||||
}
|
||||
|
||||
// Export
|
||||
module.exports = {
|
||||
logger,
|
||||
logSessionStart,
|
||||
logSessionEnd,
|
||||
logDir,
|
||||
};
|
||||
172
server/routes/download.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
const { app } = require("electron");
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { execFile } = require("child_process");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { Notification } = require("electron");
|
||||
const logger = require("../logger").logger;
|
||||
const userYtDlp = path.join(app.getPath("userData"), "yt-dlp.exe");
|
||||
|
||||
// Path vers yt-dlp
|
||||
const ytDlpPath = path.join(process.resourcesPath, "yt-dlp.exe");
|
||||
const sourceYtDlp = path.join(process.resourcesPath, "yt-dlp.exe");
|
||||
|
||||
|
||||
if (!fs.existsSync(userYtDlp)) {
|
||||
fs.copyFileSync(sourceYtDlp, userYtDlp);
|
||||
logger.info(`yt-dlp copié dans le dossier utilisateur : ${userYtDlp}`);
|
||||
}
|
||||
|
||||
// Lancement de la mise à jour une seule fois au démarrage
|
||||
execFile(userYtDlp, ["-U"], (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
logger.warn("Erreur update yt-dlp:", err);
|
||||
logger.debug(err);
|
||||
return;
|
||||
}
|
||||
logger.info(`Update yt-dlp : ${stdout}`);
|
||||
});
|
||||
|
||||
// Validation simple d'une URL
|
||||
function isValidUrl(url) {
|
||||
return /^https?:\/\/.+/.test(url); // Petit Regex qui renvoie true si l'URL est bonne et False si elle est pas bonne
|
||||
}
|
||||
|
||||
router.post("/", (req, res) => {
|
||||
try {
|
||||
const options = {
|
||||
url: req.body.url,
|
||||
audioOnly: req.body.audioOnly === "1",
|
||||
quality: req.body.quality || "best",
|
||||
};
|
||||
|
||||
if (!options.url) {
|
||||
logger.warn("Requête POST /download sans URL");
|
||||
return res.status(400).send("❌ URL manquante !");
|
||||
}
|
||||
|
||||
if (!isValidUrl(options.url)) {
|
||||
logger.warn(`URL invalide : ${options.url}`);
|
||||
return res.status(400).send("❌ URL invalide !");
|
||||
}
|
||||
|
||||
let requestedOutputFolder = req.body.savePath || req.app.locals.outputFolder;
|
||||
requestedOutputFolder = path.normalize(requestedOutputFolder);
|
||||
|
||||
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é.");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(requestedOutputFolder)) {
|
||||
fs.mkdirSync(requestedOutputFolder, { recursive: true });
|
||||
logger.info(`Dossier de sortie créé : ${requestedOutputFolder}`);
|
||||
}
|
||||
|
||||
const outputTemplate = path.join(requestedOutputFolder, "%(title)s.%(ext)s");
|
||||
|
||||
const args = [
|
||||
"--no-continue",
|
||||
// "--restrict-filenames",
|
||||
"--no-overwrites",
|
||||
|
||||
"--embed-thumbnail",
|
||||
"--add-metadata",
|
||||
"--concurrent-fragments", "8",
|
||||
"--retries", "10",
|
||||
"--fragment-retries", "10",
|
||||
"--ffmpeg-location", path.join(process.resourcesPath, "ffmpeg.exe")
|
||||
];
|
||||
|
||||
if (options.audioOnly) {
|
||||
args.push("-f", "bestaudio", "--extract-audio", "--audio-format", "mp3");
|
||||
} else
|
||||
args.push("--merge-output","mp4")
|
||||
|
||||
const qualityMap = {
|
||||
best: "bestvideo+bestaudio/best/mp4",
|
||||
medium: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
|
||||
worst: "worstvideo+worstaudio/worst/mp4",
|
||||
1080: "bestvideo[height<=1080]+bestaudio/best[height<=1080]/mp4",
|
||||
720: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
|
||||
480: "bestvideo[height<=480]+bestaudio/best[height<=480]/mp4",
|
||||
};
|
||||
|
||||
const format = qualityMap[options.quality] || "best";
|
||||
args.push("-f", format);
|
||||
|
||||
args.push("-o", outputTemplate);
|
||||
args.push(options.url);
|
||||
|
||||
logger.info(`Téléchargement demandé : url=${options.url}, audioOnly=${options.audioOnly}, quality=${options.quality}`);
|
||||
logger.info(`Commande yt-dlp : ${ytDlpPath} ${args.join(" ")}`);
|
||||
|
||||
const child = execFile(userYtDlp, args);
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
data.toString().split("\n").forEach(line => {
|
||||
if (line.trim()) logger.info(`[yt-dlp stdout] ${line.trim()}`);
|
||||
});
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
data.toString().split("\n").forEach(line => {
|
||||
if (line.trim()) logger.error(`[yt-dlp stderr] ${line.trim()}`);
|
||||
});
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
logger.error(`Erreur lancement yt-dlp : ${err.message}`);
|
||||
res.status(500).send(`❌ Erreur lors de l'exécution : ${err.message}`);
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
logger.info(`yt-dlp terminé avec code de sortie : ${code}`);
|
||||
if (code === 0) {
|
||||
const iconPath = path.join(process.resourcesPath, "confirm-icon.png");
|
||||
const notif = new Notification({
|
||||
title: "Freedom Loader",
|
||||
body: "Ton téléchargement est terminé, clique ici pour l'ouvrir.",
|
||||
icon: iconPath,
|
||||
});
|
||||
|
||||
notif.on("click", () => {
|
||||
const { shell } = require("electron");
|
||||
shell.openPath(requestedOutputFolder);
|
||||
});
|
||||
|
||||
notif.show();
|
||||
res.send("✅ Téléchargement terminé !");
|
||||
} else {
|
||||
res.status(500).send(`❌ yt-dlp a échoué avec le code : ${code}`);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
logger.error(`Erreur serveur dans /download : ${err.message}`);
|
||||
res.status(500).send(`Erreur serveur : ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
101
server/routes/info.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
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 express = require("express");
|
||||
const router = express.Router();
|
||||
const { execFile } = require("child_process");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { logger } = require("../logger");
|
||||
const { app } = require("electron");
|
||||
const userYtDlp = path.join(app.getPath("userData"), "yt-dlp.exe");
|
||||
|
||||
const ytDlpPath = path.join(__dirname, '../../ressources/yt-dlp.exe');
|
||||
|
||||
// Copie yt-dlp si besoin
|
||||
if (!fs.existsSync(userYtDlp)) {
|
||||
fs.copyFileSync(ytDlpPath, userYtDlp);
|
||||
logger.info(`yt-dlp copié dans le dossier utilisateur : ${userYtDlp}`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(ytDlpPath)) {
|
||||
logger.error(`❌ yt-dlp introuvable à ${ytDlpPath}`);
|
||||
throw new Error(`yt-dlp introuvable à ${ytDlpPath}`);
|
||||
}
|
||||
|
||||
function isValidUrl(url) {
|
||||
return typeof url === "string" && /^https?:\/\//.test(url);
|
||||
}
|
||||
|
||||
router.post("/", (req, res) => {
|
||||
const url = req.body.url;
|
||||
if (!url || !isValidUrl(url)) {
|
||||
logger.warn("Requête /info invalide ou URL manquante");
|
||||
return res.status(400).send("❌ URL invalide ou manquante");
|
||||
}
|
||||
|
||||
logger.info(`Requête /info reçue pour ${url}`);
|
||||
|
||||
const args = ["--dump-single-json", "--ignore-errors", "--yes-playlist", url];
|
||||
|
||||
execFile(userYtDlp, args, { timeout: 30_000 }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
logger.error(`Erreur exécution yt-dlp: ${error.message}`);
|
||||
if (stderr) logger.debug(`stderr: ${stderr}`);
|
||||
return res.status(500).send("❌ Impossible de récupérer les infos.");
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(stdout);
|
||||
|
||||
if (data._type === "playlist") {
|
||||
logger.info(`Playlist détectée : ${data.title} (${data.entries.length} vidéos)`);
|
||||
|
||||
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,
|
||||
webpage_url: v.webpage_url,
|
||||
duration: v.duration,
|
||||
thumbnail: v.thumbnail,
|
||||
uploader: v.uploader
|
||||
}))
|
||||
};
|
||||
|
||||
return res.json(playlistPayload);
|
||||
}
|
||||
|
||||
// Vidéo unique
|
||||
logger.info(`Vidéo unique récupérée : ${data.title}`);
|
||||
res.json({
|
||||
type: "video",
|
||||
...data
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
logger.error(`Erreur parsing JSON: ${e.message}`);
|
||||
return res.status(500).send("❌ JSON illisible.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
106
server/server.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
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 express = require("express");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { logger, logSessionStart, logSessionEnd } = require("./logger");
|
||||
const debug = require("debug")("freedom-loader:server");
|
||||
|
||||
const app = express();
|
||||
|
||||
const PORT = 8787; // Port de l'app
|
||||
|
||||
// Dossier de téléchargement Windows
|
||||
const downloadsPath = path.join(process.env.USERPROFILE, "Downloads");
|
||||
const outputFolder = path.join(downloadsPath, "Freedom Loader");
|
||||
|
||||
// Création du dossier si inexistant
|
||||
if (!fs.existsSync(outputFolder)) {
|
||||
try {
|
||||
fs.mkdirSync(outputFolder, { recursive: true });
|
||||
logger.info("Dossier Freedom Loader créé dans Téléchargements.");
|
||||
} catch (err) {
|
||||
logger.error("Impossible de créer le dossier :", err);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
logger.info("Dossier Freedom Loader déjà existant.");
|
||||
}
|
||||
|
||||
app.locals.outputFolder = outputFolder;
|
||||
|
||||
app.use(express.urlencoded({ extended: true })); // Middleware pour parser les POST en x-www-form-urlencoded
|
||||
|
||||
// Fichiers statiques (frontend)
|
||||
const staticPath = path.join(__dirname, "../public");
|
||||
debug("Serveur statique sur", staticPath);
|
||||
app.use(express.static(staticPath));
|
||||
|
||||
// Routes API
|
||||
const infoRoute = require("./routes/info");
|
||||
const downloadRoute = require("./routes/download");
|
||||
debug("Routes /download et /info installées");
|
||||
app.use("/download", downloadRoute);
|
||||
app.use("/info", infoRoute);
|
||||
|
||||
// Route principale
|
||||
app.get("/", (req, res) => {
|
||||
debug("Requête GET / servie");
|
||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||
});
|
||||
|
||||
// Fonction pour démarrer le serveur Express
|
||||
async function startServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
logSessionStart(); // Début de session
|
||||
|
||||
const serverInstance = app.listen(PORT, () => {
|
||||
logger.info(`Serveur Express prêt sur http://localhost:${PORT}`);
|
||||
resolve(serverInstance);
|
||||
});
|
||||
|
||||
// Gestion des erreurs serveur
|
||||
serverInstance.on("error", (err) => {
|
||||
logger.error("Erreur serveur Express :", err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// Gestion propre de la fermeture du serveur
|
||||
const gracefulExit = () => {
|
||||
logSessionEnd();
|
||||
serverInstance.close(() => {
|
||||
logger.info("Serveur Express fermé proprement.");
|
||||
process.exit();
|
||||
});
|
||||
};
|
||||
|
||||
process.on("SIGINT", gracefulExit);
|
||||
process.on("SIGTERM", gracefulExit);
|
||||
});
|
||||
}
|
||||
|
||||
// Export de startServer
|
||||
module.exports = { startServer };
|
||||
|
||||
// Lancement du Discord RPC après que le serveur soit prêt
|
||||
(async () => {
|
||||
const { startRPC } = require("./discordRPC");
|
||||
await startServer(); // on attend que le serveur démarre
|
||||
startRPC();
|
||||
})();
|
||||