1.4.0 - It's time to sleep

1.4.0 - It's time to sleep
This commit is contained in:
MasterAcnolo
2026-01-22 22:09:24 +01:00
committed by GitHub
37 changed files with 1595 additions and 1010 deletions

View File

@@ -1,13 +1,12 @@
<div align="center">
<img
<a href="https://masteracnolo.github.io/FreedomLoader/"><img
src="./build/banner.png"
alt="Banner"
style="width: 50%;;"/>
style="width: 50%;;"/></a>
</div>
<div align="center">
<a href="https://github.com/MasterAcnolo/Freedom-Loader/releases"><img src="https://img.shields.io/badge/Release-1.3.1-blue?style=for-the-badge"></a>
<a href="https://github.com/MasterAcnolo/Freedom-Loader/releases"><img src="https://img.shields.io/badge/Release-1.4.0-blue?style=for-the-badge"></a>
<a href="https://masteracnolo.github.io/FreedomLoader/index.html"><img src="https://img.shields.io/badge/Freedom%20Loader-Open%20Page-111111?style=for-the-badge&logo=terminal&logoColor=white"></a>
<a href="https://www.firefox.com/fr/?utm_campaign=SET_DEFAULT_BROWSER"><img src="https://img.shields.io/badge/Require Firefox-E66000?style=for-the-badge&logo=Firefox-Browser&logoColor=white"></a>
</div>

BIN
build/banner.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View File

@@ -1,10 +1,26 @@
const packageJson = require("./package.json");
const { app } = require("electron");
const fs = require("fs");
const path = require("path");
const localMode = !app.isPackaged;
const featuresPath = localMode ? path.join(__dirname, "./config/config.json") : path.join(path.join(process.resourcesPath,"config/" ,"config.json"));
let features = {};
function loadFeatures() {
const raw = fs.readFileSync(featuresPath, "utf-8");
features = JSON.parse(raw);
return features;
}
const configFeatures = loadFeatures();
module.exports = {
version: packageJson.version,
applicationPort: "8787",
debugMode: true,
localMode: !app.isPackaged,
localMode,
DiscordRPCID: "1410934537051181146",
configFeatures
}

15
config/config.json Normal file
View File

@@ -0,0 +1,15 @@
{
"autoUpdate": true,
"discordRPC": true,
"customTopBar": true,
"autoCheckInfo": true,
"addThumbnail": true,
"addMetadata": true,
"verboseLogs": false,
"autoDownloadPlaylist": true,
"customCodec": "h264",
"logSystem": true,
"outputTitleCheck": true,
"downloadSystem": true,
"notifySystem": true
}

109
main.js
View File

@@ -3,16 +3,27 @@ const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron")
const path = require("path");
const os = require("os");
const fs = require("fs");
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
const { AutoUpdater } = require("./server/update.js");
const { configFeatures } = require("./config.js");
const { startRPC } = require("./server/discordRPC");
let mainWindow;
const logsFolderPath = logDir;
const basePath = config.localMode
? path.join(__dirname )
: path.join(path.dirname(process.execPath), "resources");
const configFolderPath = path.join(basePath, "config" ,"config.json");
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
app.disableHardwareAcceleration();
ipcMain.handle("version", () => config.version);
// Gestion single instance
const gotLock = app.requestSingleInstanceLock();
@@ -20,10 +31,10 @@ const gotLock = app.requestSingleInstanceLock();
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
function checkNativeDependencies() {
const deps = [
{ name: "yt-dlp.exe", path: path.join(process.resourcesPath, "yt-dlp.exe") },
{ name: "ffmpeg.exe", path: path.join(process.resourcesPath, "ffmpeg.exe") },
{ name: "ffprobe.exe", path: path.join(process.resourcesPath, "ffprobe.exe") },
{ name: "deno.exe", path: path.join(process.resourcesPath, "deno.exe") },
{ name: "yt-dlp.exe", path: path.join(process.resourcesPath, "binaries","yt-dlp.exe") },
{ name: "ffmpeg.exe", path: path.join(process.resourcesPath, "binaries", "ffmpeg.exe") },
{ name: "ffprobe.exe", path: path.join(process.resourcesPath, "binaries", "ffprobe.exe") },
{ name: "deno.exe", path: path.join(process.resourcesPath, "binaries", "deno.exe") },
];
const missing = deps.filter(dep => !fs.existsSync(dep.path));
let errorMsg = "";
@@ -77,7 +88,7 @@ async function createMainWindow() {
height: 800,
minWidth: 750,
minHeight: 800,
frame:false,
frame: !configFeatures.customTopBar,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
@@ -98,21 +109,44 @@ async function createMainWindow() {
});
}
function validateDownloadPath(userPath) {
const userHome = os.homedir(); // C:\Users\<User>
if (!userPath) return path.join(userHome, "Downloads", "Freedom Loader");
// Résolution canonique et suivi des symlinks
const resolved = fs.realpathSync(path.resolve(userPath));
const normalizedHome = path.resolve(userHome) + path.sep;
if (!resolved.startsWith(normalizedHome)) {
throw new Error("Chemin non autorisé : uniquement les sous-dossiers du dossier utilisateur sont permis !");
}
return resolved;
}
// 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];
const validatedPath = validateDownloadPath(result.filePaths[0]);
logger.info(`Folder Checked and Valid : ${validatedPath}`);
return validatedPath;
}
return null;
} catch (err) {
logger.error(`Error when creating Output Folder : ${err.message}`);
logger.error(`An Error Occured when validating folder : ${err.message}`);
return null;
}
});
ipcMain.handle("validate-download-path", (event, userPath) => {
return validateDownloadPath(userPath);
});
ipcMain.handle("get-default-download-path", () => defaultDownloadPath);
ipcMain.on("set-progress", (event, percent) => {
@@ -149,6 +183,9 @@ ipcMain.on("open-website", () => {
ipcMain.on("open-wiki", () => {
shell.openExternal("https://masteracnolo.github.io/FreedomLoader/pages/wiki.html");
});
ipcMain.on("open-config", () => {
if (configFolderPath) shell.openPath(configFolderPath);
});
// App ready
@@ -156,18 +193,66 @@ app.whenReady().then(async () => {
logSessionStart();
logger.info("App Ready, Server Express starting...");
const serverPath = path.join(__dirname, "server", "server.js");
const serverPath = path.join(__dirname, "server", "server.js")
const expressServer = require(serverPath);
try {
await expressServer.startServer();
logger.info("Express Server Started");
const { startRPC } = require("./server/discordRPC");
startRPC();
ipcMain.handle("features", () => {
return configFeatures;
});
const featureWhitelist = new Set([
"autoUpdate",
"discordRPC",
"customTopBar",
"autoCheckInfo",
"addThumbnail",
"addMetadata",
"verboseLogs",
"autoDownloadPlaylist",
"customCodec"
]);
ipcMain.handle("set-feature", (event, { key, value }) => {
try {
if (!featureWhitelist.has(key)) {
logger.warn(`Rejected feature (not whitelisted): ${key}`);
return false;
}
// optionnel mais propre
if (configFeatures[key] === value) {
return true;
}
configFeatures[key] = value;
fs.writeFileSync(
configFolderPath,
JSON.stringify(configFeatures, null, 2),
"utf-8"
);
logger.info(`Feature updated: ${key} = ${value}`);
return true;
} catch (err) {
logger.error(`set-feature failed (${key}): ${err.message}`);
return false;
}
});
configFeatures.discordRPC ? startRPC() : "";
await createMainWindow();
AutoUpdater(mainWindow);
configFeatures.autoUpdate ? AutoUpdater(mainWindow) : ""; // Auto Update
} catch (err) {
logger.error("Window or Server error :", err);
app.quit();

1297
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,22 @@
{
"name": "freedom-loader",
"productName": "Freedom Loader",
"version": "1.3.1",
"version": "1.4.0",
"author": "MasterAcnolo",
"description": "Freedom Loader",
"main": "main.js",
"scripts": {
"start": "electron --trace-warnings .",
"dist": "electron-builder",
"build": "electron-builder",
"update": "npm update"
},
"dependencies": {
"chalk": "^4.1.2",
"debug": "^4.4.1",
"discord-rpc": "^4.0.1",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.6.2",
"express": "^5.1.0",
"express-rate-limit": "^8.2.1",
"webidl-conversions": "^8.0.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
@@ -38,9 +38,7 @@
"files": [
"**/*",
"node_modules/**/*",
"!logs/*.json",
"server/**/*",
"server/node_modules/**/*"
"!logs/*.json"
],
"directories": {
"buildResources": "build"
@@ -55,29 +53,33 @@
"from": "build/app-icon.ico",
"to": "app-icon.ico"
},
{
"from": "build/banner.bmp",
"to": "banner.bmp"
},
{
"from": "build/error.png",
"to": "error.png"
},
{
"from": "ressources/yt-dlp.exe",
"to": "yt-dlp.exe"
"to": "binaries/yt-dlp.exe"
},
{
"from": "ressources/ffmpeg.exe",
"to": "ffmpeg.exe"
"to": "binaries/ffmpeg.exe"
},
{
"from": "ressources/ffprobe.exe",
"to": "ffprobe.exe"
"to": "binaries/ffprobe.exe"
},
{
"from": "ressources/deno.exe",
"to": "deno.exe"
"to": "binaries/deno.exe"
},
{
"from": "server",
"to": "server"
"from": "config/config.json",
"to": "config/config.json"
}
],
"win": {
@@ -90,6 +92,7 @@
"allowToChangeInstallationDirectory": true,
"installerIcon": "build/app-icon.ico",
"uninstallerIcon": "build/uninstaller-icon.ico",
"installerSidebar": "build/banner.bmp",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "Freedom Loader"

View File

@@ -3,7 +3,11 @@ const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
setProgress: (percent) => ipcRenderer.send("set-progress", percent)
setProgress: (percent) => ipcRenderer.send("set-progress", percent),
getFeatures: () => ipcRenderer.invoke("features"),
setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
getVersion: () => ipcRenderer.invoke("version"),
getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path)
});
// Contrôles de fenêtre et outils custom pour la topbar
@@ -14,5 +18,6 @@ contextBridge.exposeInMainWorld("topbarAPI", {
openDevTools: () => ipcRenderer.send("open-devtools"),
openLogs: () => ipcRenderer.send("open-logs"),
openWebsite: () => ipcRenderer.send("open-website"),
openWiki: () => ipcRenderer.send("open-wiki")
openWiki: () => ipcRenderer.send("open-wiki"),
openConfig: () => ipcRenderer.send("open-config")
});

View File

@@ -17,7 +17,7 @@
</head>
<body>
<div class="topbar">
<div class="topbar" id="topbar">
<div class="custom-controls">
<button id="devtools-btn" title="Dev Tools">Tools</button>
<button id="logs-btn" title="Logs">Logs</button>
@@ -31,12 +31,174 @@
</div>
</div>
<div class="theme-switcher">
<label for="themeSelect">Thème :</label>
<select id="themeSelect" aria-label="Choisir le thème"></select>
<button class="settings-button" id="settings-button">
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><title>ionicons-v5-q</title><path d="M262.29,192.31a64,64,0,1,0,57.4,57.4A64.13,64.13,0,0,0,262.29,192.31ZM416.39,256a154.34,154.34,0,0,1-1.53,20.79l45.21,35.46A10.81,10.81,0,0,1,462.52,326l-42.77,74a10.81,10.81,0,0,1-13.14,4.59l-44.9-18.08a16.11,16.11,0,0,0-15.17,1.75A164.48,164.48,0,0,1,325,400.8a15.94,15.94,0,0,0-8.82,12.14l-6.73,47.89A11.08,11.08,0,0,1,298.77,470H213.23a11.11,11.11,0,0,1-10.69-8.87l-6.72-47.82a16.07,16.07,0,0,0-9-12.22,155.3,155.3,0,0,1-21.46-12.57,16,16,0,0,0-15.11-1.71l-44.89,18.07a10.81,10.81,0,0,1-13.14-4.58l-42.77-74a10.8,10.8,0,0,1,2.45-13.75l38.21-30a16.05,16.05,0,0,0,6-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16,16,0,0,0-6.07-13.94l-38.19-30A10.81,10.81,0,0,1,49.48,186l42.77-74a10.81,10.81,0,0,1,13.14-4.59l44.9,18.08a16.11,16.11,0,0,0,15.17-1.75A164.48,164.48,0,0,1,187,111.2a15.94,15.94,0,0,0,8.82-12.14l6.73-47.89A11.08,11.08,0,0,1,213.23,42h85.54a11.11,11.11,0,0,1,10.69,8.87l6.72,47.82a16.07,16.07,0,0,0,9,12.22,155.3,155.3,0,0,1,21.46,12.57,16,16,0,0,0,15.11,1.71l44.89-18.07a10.81,10.81,0,0,1,13.14,4.58l42.77,74a10.8,10.8,0,0,1-2.45,13.75l-38.21,30a16.05,16.05,0,0,0-6.05,14.08C416.17,247.67,416.39,251.83,416.39,256Z" style="fill:none;stroke:var(--settings-button-text-color);stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>
</button>
<div class="settings-panel" id="settings-panel">
<div class="settings-modal">
<button class="close-btn" onclick="closePanel()"></button>
<div class="settings-title">
<h2>SETTINGS</h2>
<p style="font-size: 0.8em;">Changes are saved automatically. <br> Somes changes could need a restart</p>
</div>
<div class="container">
<!-- Basic Settings visibles -->
<div class="settings-section">
<h3>Basic Settings</h3>
<div class="setting-item" id="theme" style="margin-left: 32px;">
<div class="setting-info">
<div style="display: flex;">
<span class="setting-title">Thème</span>
<select id="themeSelect" data-key="theme" aria-label="Choisir le thème">
</select>
</div>
<small>Choose the theme of the application</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="discordRPC" id="discordRPC">
<div class="setting-info">
<span class="setting-title">Discord RPC</span>
<small>Enable/disable Discord Rich Presence for your app.</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="autoDownloadPlaylist" id="autoDownloadPlaylist">
<div class="setting-info">
<span class="setting-title">Download Playlist automatically?</span>
<small>If enabled, playlists will download without confirmation.</small>
</div>
</div>
<!-- <div class="setting-item">
<input type="checkbox" data-key="notifySystem" id="notifySystem">
<div class="setting-info">
<span class="setting-title">Notification at the end</span>
<small>Notify when a download finishes.</small>
</div>
</div> -->
<div class="setting-item">
<input type="checkbox" data-key="customTopBar" id="customTopBar">
<div class="setting-info">
<span class="setting-title">Custom Top bar</span>
<small>Use this if the topbar is missing or you prefer a custom one.</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="autoCheckInfo" id="autoCheckInfo">
<div class="setting-info">
<span class="setting-title">Auto Fetch Info</span>
<small>Automatically fetch information for new downloads.</small>
</div>
</div>
</div>
<!-- Advanced Settings -->
<details class="settings-section">
<summary>Advanced Settings</summary>
<div class="setting-item">
<input type="checkbox" data-key="autoUpdate" id="autoUpdate">
<div class="setting-info">
<span class="setting-title">Auto Update</span>
<small>Enable automatic app updates.</small>
</div>
</div>
<!-- <div class="setting-item">
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
<div class="setting-info">
<span class="setting-title">Output title (special chars)</span>
<small>Check for unusual characters in output titles.</small>
</div>
</div> -->
<div class="setting-item">
<input type="checkbox" data-key="addMetadata" id="addMetadata">
<div class="setting-info">
<span class="setting-title">Add Metadata</span>
<small>Include metadata in downloaded files.</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="addThumbnail" id="addThumbnail">
<div class="setting-info">
<span class="setting-title">Add Thumbnail</span>
<small>Include thumbnails in downloaded files.</small>
</div>
</div>
<div class="setting-item" style="margin-left: 32px; margin-top: 20px;">
<div class="setting-info">
<span class="setting-title">Custom Codec</span>
<select data-key="customCodec">
<option value="h264">H264</option>
<option value="av1">AV1</option>
<option value="av01">AV01</option>
<option value="vp9.2">VP9.2</option>
<option value="vp9">VP9</option>
<option value="h265">H265</option>
<option value="theora">Theora</option>
</select>
<small>Choose your preferred video codec.</small>
</div>
</div>
</details>
<!-- Developer Settings -->
<details class="settings-section">
<summary>Developer Settings</summary>
<!-- <div class="setting-item">
<input type="checkbox" data-key="downloadSystem" id="downloadSystem">
<div class="setting-info">
<span class="setting-title">Download System</span>
<small>Enable/disable the internal download system.</small>
</div>
</div> -->
<!-- <div class="setting-item">
<input type="checkbox" data-key="logSystem" id="logSystem">
<div class="setting-info">
<span class="setting-title">Log System</span>
<small>Enable logging system.</small>
</div>
</div> -->
<div class="setting-item">
<input type="checkbox" data-key="verboseLogs" id="verboseLogs">
<div class="setting-info">
<span class="setting-title">Verbose Logs</span>
<small>Show verbose logs for debugging purposes.</small>
</div>
</div>
</details>
<div class="settings-footer">
<button id="open-json-btn">Open JSON Configuration</button>
</div>
</div>
</div>
<div class="container" id="container">
<header>
<h1 id="title"> Freedom Loader </h1>
@@ -95,10 +257,15 @@
<div id="videoInfo" style="margin-top: 20px;"></div>
</div>
<span id="version-badge" class="version-badge">v1.3.1</span>
<span id="version-badge" class="version-badge"></span>
<h2 id="reference"> Because Why Not ? </h2>
<!-- Scripts -->
<script src="script/custompath.js"></script>
<script src="script/settingsPanel.js"></script>
<script src="script/appVersion.js"></script>
<script src="script/downloadstatus.js"></script>
<script src="script/fetchinfo.js"></script>
<script src="script/progressBar.js"></script>

View File

@@ -0,0 +1,6 @@
async function versionLabel(){
const appVersion = await window.electronAPI.getVersion();
document.getElementById("version-badge").textContent = `v${appVersion}`;
};
versionLabel();

View File

@@ -1,41 +1,62 @@
window.addEventListener("DOMContentLoaded", async () => {
const savePathElem = document.getElementById("savePath");
const form = document.getElementById("downloadForm");
// Essayer de charger depuis le localStorage
let savedPath = localStorage.getItem("customDownloadPath");
// Sinon demander le chemin par défaut à l'API Electron
if (!savedPath) {
savedPath = await window.electronAPI.getDefaultDownloadPath();
}
// Afficher le chemin
if (savePathElem) {
savePathElem.textContent = savedPath;
}
// Créer l'input caché s'il n'existe pas déjà
// input caché = DERNIER chemin validé par le back
let hidden = document.getElementById("savePathInput");
if (!hidden) {
hidden = document.createElement("input");
hidden.type = "hidden";
hidden.name = "savePath";
hidden.id = "savePathInput";
document.getElementById("downloadForm").appendChild(hidden);
form.appendChild(hidden);
}
hidden.value = savedPath;
// Gestion du bouton de modification
document.getElementById("changePath").addEventListener("click", async () => {
const selectedPath = await window.electronAPI.selectDownloadFolder();
if (selectedPath) {
// Met à jour l'affichage
savePathElem.textContent = selectedPath;
hidden.value = selectedPath;
async function applyPathFromBack(path) {
savePathElem.textContent = path;
hidden.value = path;
localStorage.setItem("customDownloadPath", path); // UX only
}
// Et le stocke en localStorage pour la prochaine fois
localStorage.setItem("customDownloadPath", selectedPath);
async function loadInitialPath() {
// On affiche ce que le user a vu la dernière fois (UX)
const cached = localStorage.getItem("customDownloadPath");
if (cached) {
savePathElem.textContent = cached;
}
// MAIS la source de vérité reste le back
try {
const validatedPath = await window.electronAPI.getValidatedDownloadPath(
cached
);
await applyPathFromBack(validatedPath);
} catch {
// fallback sûr
const defaultPath =
await window.electronAPI.getDefaultDownloadPath();
await applyPathFromBack(defaultPath);
}
}
await loadInitialPath();
document
.getElementById("changePath")
.addEventListener("click", async () => {
try {
const selectedPath =
await window.electronAPI.selectDownloadFolder();
if (!selectedPath) return; // annulé
// Validation back obligatoire
const validatedPath =
await window.electronAPI.getValidatedDownloadPath(selectedPath);
await applyPathFromBack(validatedPath);
} catch (err) {
alert("Dossier non autorisé.");
}
});
});

View File

@@ -1,8 +1,7 @@
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 ?"},
neon: { label: "Purple", subtitle: "How was your day ?"},
nf: { label: "NF", subtitle: "You call it music, i call it my Therapist" },
songbird: { label: "Songbird", subtitle: "From Her to Eternity" },
drift: { label: "Drift", subtitle: "Si la route t'appelle, contre appel" },

View File

@@ -26,6 +26,9 @@ async function fetchVideoInfo(url) {
}
}
async function init() {
document.addEventListener("DOMContentLoaded", () => {
const urlInput = document.getElementById("UrlInput");
const infoDiv = document.getElementById("videoInfo");
@@ -34,6 +37,10 @@ document.addEventListener("DOMContentLoaded", () => {
let lastFetchedUrl = "";
urlInput.addEventListener("input", async () => {
const configFeatures = await window.electronAPI.getFeatures();
if (!configFeatures.autoCheckInfo) return;
const url = urlInput.value.trim();
// Si champ vide -> reset total
@@ -178,3 +185,7 @@ document.addEventListener("DOMContentLoaded", () => {
});
})
};
init();

View File

@@ -0,0 +1,50 @@
const settingsPanel = document.getElementById("settings-panel");
const settingsModal = document.querySelector(".settings-modal");
const settingsButton = document.getElementById("settings-button");
let isOpen = false;
settingsButton.addEventListener("click", () => {
const isOpen = settingsPanel.style.display === "block";
settingsPanel.style.display = isOpen ? "none" : "block";
});
function closePanel() {
isOpen = false;
settingsPanel.style.display = "none";
}
// charge les features
async function loadSettings() {
const features = await window.electronAPI.getFeatures();
document.querySelectorAll(".settings-section input, .settings-section select").forEach(el => {
const key = el.dataset.key;
if (features[key] !== undefined) {
if (el.type === "checkbox") el.checked = features[key];
else if (el.tagName === "SELECT") el.value = features[key];
}
el.addEventListener("change", () => {
let value = el.type === "checkbox" ? el.checked : el.value;
window.electronAPI.setFeature(key, value);
});
});
}
// ouvrir le JSON
document.getElementById("open-json-btn").addEventListener("click", () => {
window.topbarAPI.openConfig(); // ton IPC existant
});
/* clic sur l'overlay = fermer */
settingsPanel.addEventListener("click", closePanel);
/* clic dans la modal = stop propagation */
settingsModal.addEventListener("click", (e) => {
e.stopPropagation();
});
loadSettings();

View File

@@ -10,6 +10,7 @@ function setupTopbarListeners() {
const logsBtn = document.getElementById('logs-btn');
const websiteBtn = document.getElementById('website-btn');
const wikiBtn = document.getElementById('wiki-btn');
const configBtn = document.getElementById('config-btn');
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
@@ -18,7 +19,16 @@ function setupTopbarListeners() {
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
if (configBtn) configBtn.onclick = () => topbarAPI.openConfig();
});
}
setupTopbarListeners();
setupTopbarListeners(); // IF it put it the if check. It don't work. Why ?
const features = await window.electronAPI.getFeatures();
if(!features.customTopBar){
document.getElementById("topbar").style.display = "none";
document.getElementById("container").style.marginTop = "0";
document.getElementById("theme-switcher").style.top = "30px";
}

View File

@@ -1,38 +0,0 @@
.theme-switcher {
position: absolute;
top: 60px;
right: 10px;
background: #242424;
padding: 8px 12px;
border-radius: 8px;
color: #fff;
font-family: sans-serif;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 8px;
z-index: 1050;
}
.theme-switcher label {
cursor: pointer;
font-weight: 600;
}
.theme-switcher select {
background: #fff;
border: 1px solid #242424;
border-radius: 6px;
padding: 4px 8px;
color: #000;
cursor: pointer;
font-weight: 600;
transition: background 0.3s ease, color 0.3s ease;
}
.theme-switcher select:hover,
.theme-switcher select:focus {
background: #474747;
color: #fff;
outline: none;
}

View File

@@ -0,0 +1,234 @@
/* SETTINGS BUTTON */
.settings-button {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 60px;
right: 20px;
opacity: 0.8;
background-color: var(--settings-button-bg-color);
border-radius: 14px;
padding: 8px;
border: none;
box-shadow: 0 6px 16px rgba(0,0,0,0.25);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.settings-button svg {
width: 40px;
height: 40px;
stroke: var(--settings-button-text-color);
}
.settings-button:hover {
transform: scale(1.1);
cursor: pointer;
box-shadow: 0 10px 24px rgba(0,0,0,0.35);
}
.settings-button:active {
transform: scale(0.95);
}
/* SETTINGS PANEL */
.settings-panel {
position: fixed;
top: 60px;
right: 40px;
width: 40vw;
max-width: 320px;
max-height: 80vh;
background-color: var(--settings-bg-primary-color);
border-radius: 12px;
padding: 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
display: none;
overflow-y: auto;
z-index: 9999;
animation: slideIn 0.2s ease-out;
color: var(--settings-text-color);
}
.settings-panel::-webkit-scrollbar {
width: 0px;
background: transparent;
}
.settings-modal {
width: 100%;
min-height: auto;
max-height: none;
padding: 0;
background: none;
box-shadow: none;
border-radius: none;
}
/* TITLE */
.settings-title {
margin-bottom: 20px;
}
.settings-title h2 {
margin: 0;
font-size: 24px;
}
.settings-title p {
font-size: 14px;
color: #ccc;
margin-top: 4px;
}
/* SECTION */
.settings-section {
display: flex;
flex-direction: column;
margin-bottom: 20px;
padding: 10px 15px;
background: var(--settings-bg-secondary-color);
border-radius: 12px;
gap: 6px;
}
.settings-section h3 {
margin: 0 0 10px 0;
font-size: 18px;
}
/* SETTING ITEM */
.setting-item {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 8px;
margin-top: 8px;
}
.setting-item input[type="checkbox"] {
margin: 0;
flex-shrink: 0;
width: 20px;
height: 20px;
}
.setting-info {
display: flex;
flex-direction: column;
}
.setting-title {
font-size: 14px;
font-weight: 500;
}
.setting-info small {
font-size: 12px;
color: var(--settings-subtitle-color);
margin-top: 2px;
font-style: italic;
}
#theme .settings-title{
display: flex;
}
.setting-item select {
width: 70%;
background: #fff;
border: 1px solid #242424;
border-radius: 6px;
padding: 4px 8px;
color: #000;
cursor: pointer;
font-weight: 600;
transition: background 0.3s ease, color 0.3s ease;
}
.setting-item select:hover,
.setting-item select:focus {
background: #474747;
color: #fff;
outline: none;
}
/* SELECT */
.settings-section select {
margin-left: 10px;
}
/* DETAILS / DROPDOWN */
details {
margin-bottom: 20px;
padding: 10px 15px;
background: #2a2a2a;
border-radius: 12px;
}
details[open] {
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
details summary {
cursor: pointer;
}
/* FOOTER */
.settings-footer {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
#open-json-btn {
padding: 6px 12px;
border-radius: 8px;
border: none;
cursor: pointer;
font-weight: 600;
background-color: var(--form-button-bg-color);
color: #fff;
}
#experimental-args {
width: 100%;
min-height: 80px;
background-color: #1e1e1e;
border-radius: 8px;
padding: 8px;
color: #fff;
border: 1px solid #333;
resize: vertical;
}
/* CLOSE BTN */
.close-btn {
position: absolute;
top: 20px;
right: 20px;
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
opacity: 0.6;
}
.close-btn:hover {
opacity: 1;
}
/* ANIMATION */
@keyframes popIn {
from { transform: scale(0.95); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes slideIn {
from { transform: translateX(20px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}

View File

@@ -1,12 +1,12 @@
/* Layout */
@import url("layout/form.css");
@import url("layout/settingsPanel.css");
@import url("layout/container.css");
@import url("layout/header.css");
@import url("layout/videoinfo.css");
/* Components */
@import url("components/checkbox.css");
@import url("components/themebutton.css");
@import url("components/loader.css");
@import url("theme/themeimport.css");
@@ -49,6 +49,16 @@ body {
}
/* Because why not */
#reference{
position: fixed;
pointer-events: none;
bottom:0vh;
left: 10px;
font-size: 1em;
color: #b0b0b02f;
}
/* Scrollbar globale pour Chromium/Electron */
body::-webkit-scrollbar {
width: 10px;

View File

@@ -42,6 +42,16 @@ body.chirac {
/* Playlist Color */
--playlist-background-color: #d1d1d1ca;
/* Settings */
--settings-button-bg-color: #00196a;
--settings-button-text-color: #dcdcdc;
--settings-bg-primary-color: #0b0528;
--settings-bg-secondary-color: #1c1055;
--settings-text-color: #cecece;
--settings-subtitle-color: #adadad;
}

View File

@@ -36,4 +36,14 @@ body.dark {
/* Playlist Color */
--playlist-background-color: #303030;
/* Settings */
--settings-button-bg-color: #1e1e1e;
--settings-button-text-color: var(--default-text-color);
--settings-bg-primary-color: #1e1e1e;
--settings-bg-secondary-color: #2a2a2a;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #aaa;
}

View File

@@ -1,43 +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: #0c0140;
--video-info-text-color: #ffffff;
--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;
/* Playlist Color */
--playlist-background-color: #070823a3;
}
body.default .download-path #savePath {
color: #ffffff;
}

View File

@@ -42,6 +42,16 @@ body.drift {
/* Playlist Color */
--playlist-background-color: #424b5cc2;
/* Settings */
--settings-button-bg-color: #1e222ace;
--settings-button-text-color: #b3b3b4;
--settings-bg-primary-color: #1e222a;
--settings-bg-secondary-color: #272d38;
--settings-text-color: #bababa;
--settings-subtitle-color: #cacaca;
}

View File

@@ -36,4 +36,14 @@ body.fanatic{
/* Playlist Color */
--playlist-background-color: #2b2b2b;
/* Settings */
--settings-button-bg-color: #1e1e1e;
--settings-button-text-color: #dcdcdc;
--settings-bg-primary-color: #1e1e1e;
--settings-bg-secondary-color: #2d2d2d;
--settings-text-color: #cecece;
--settings-subtitle-color: #adadad;
}

View File

@@ -36,4 +36,13 @@ body.light {
/* Playlist Color */
--playlist-background-color: #c5c5c5b2;
/* Settings */
--settings-button-bg-color: #e4e4e4;
--settings-button-text-color: var(--default-text-color);
--settings-bg-primary-color: #cbcbcb;
--settings-bg-secondary-color: #cbcbcb;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #656565;
}

View File

@@ -43,6 +43,15 @@ body.neon {
/* Playlist Color */
--playlist-background-color: #3e3e64;
/* Settings */
--settings-button-bg-color: #202038;
--settings-button-text-color: var(--default-text-color);
--settings-bg-primary-color: #242454;
--settings-bg-secondary-color: #2f2f6f;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #d2d2d2;
}
body.neon .download-path {

View File

@@ -42,6 +42,15 @@ body.nf {
/* Playlist Color */
--playlist-background-color: #4f4f4f;
/* Settings */
--settings-button-bg-color: #2a2a2a;
--settings-button-text-color: var(--default-text-color);
--settings-bg-primary-color: #2a2a2a;
--settings-bg-secondary-color: #313131;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #7a7979;
}
body.nf .download-path {

View File

@@ -42,6 +42,15 @@ body.songbird {
/* Playlist Color */
--playlist-background-color: #3f0000;
/* Settings */
--settings-button-bg-color: rgba(29, 0, 0, 0.784);
--settings-button-text-color: var(--default-text-color);
--settings-bg-primary-color: #121212;
--settings-bg-secondary-color: #1a1a1a;
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #7a7979;
}
body.cyberpunk .download-path {

View File

@@ -36,6 +36,17 @@ body.spicy {
/* Playlist Color */
--playlist-background-color: #353232;
/* Settings */
--settings-button-bg-color: #1a1a1a;
--settings-button-text-color: #dcdcdc;
--settings-bg-primary-color: #1a1a1a;
--settings-bg-secondary-color: #292929;
--settings-text-color: #cecece;
--settings-subtitle-color: #adadad;
}
/* Optionnel: background piments */

View File

@@ -1,6 +1,5 @@
@import url("chirac-theme.css");
@import url("dark-theme.css");
@import url("default-theme.css");
@import url("fanatic-theme.css");
@import url("light-theme.css");
@import url("nf-theme.css");

View File

@@ -33,4 +33,14 @@
--video-info-list-strong-color: #000000;
--video-info-link-color: #007bff;
--video-info-link-hover-color: #0056b3;
/* Settings */
--settings-button-bg-color: var(--form-bg-color);
--settings-button-text-color: var(--default-text-color);
--settings-bg-primary-color: var(--form-bg-color);
--settings-bg-secondary-color: var(--form-bg-color);
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #aaa;
}

View File

@@ -1,7 +1,6 @@
const { fetchDownload } = require("../services/download.services");
const { logger } = require("../logger");
const { isValidUrl, isSafePath } = require("../helpers/validation");
const { buildYtDlpArgs } = require("../helpers/buildArgs");
const { notifyDownloadFinished } = require("../helpers/notify");
const listeners = [];
@@ -25,7 +24,7 @@ async function downloadController(req, res) {
} catch (err) {
logger.error(`Server Error in /download : ${err.message}`);
res.status(500).send(`${err.message}`);
res.status(500).send(`Server Error`);
}
}

View File

@@ -1,6 +1,3 @@
const express = require("express");
const router = express.Router();
const { fetchInfo } = require("../services/info.services");
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo");
const { logger } = require("../logger");
@@ -12,7 +9,7 @@ async function infoController(req, res){
/* Si pas d'url ou url invalide*/
if (!url || !isValidUrl(url)) return res.status(400).send("❌ Invalid URL Or Missing");
if (!url || typeof url !== "string" || !isValidUrl(url)) return res.status(400).send("❌ Invalid URL Or Missing");
logger.info(`/Info Request receive by the Info Controller. URL: ${url}`);
@@ -40,7 +37,8 @@ async function infoController(req, res){
} catch (err) {
return res.status(500).send(`${err.message}`);
logger.error(`Info controller error: ${err && err.message ? err.message : err}`);
return res.status(500).send(`❌ Unable to fetch info`);
}
}

View File

@@ -2,22 +2,64 @@ const path = require("path");
const getUserBrowser = require("./getBrowser");
const { ffmpegPath, denoPath} = require("./path");
const { configFeatures } = require("../../config.js");
const { logger } = require("../logger");
function validateCodec(codec){
const validCodec = [
"av01","vp9.2", "vp9" , "h265" , "h264" , "vp8" , "h263", "theora"
]
if(validCodec.includes(codec)){
logger.info(`Codec Valid: ${codec}`)
return codec
} else{
logger.error(`Codec not valid: ${codec}. Using default codec`)
return "h264"
}
}
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
logger.info("--- CONFIGURATION ---");
logger.info(`CONFIG autoUpdate: ${configFeatures.autoUpdate}`);
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);
logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`);
logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`);
logger.info(`CONFIG autoDownloadPlaylist: ${configFeatures.autoDownloadPlaylist}`);
logger.info(`CONFIG customCodec: ${configFeatures.customCodec}`);
// ACTIVATE THEM WHEN THE FEATURE IS IMPLEMENTED
// logger.info(`CONFIG logSystem: ${configFeatures.logSystem}`);
// logger.info(`CONFIG outputTitleCheck: ${configFeatures.outputTitleCheck}`);
// logger.info(`CONFIG downloadSystem: ${configFeatures.downloadSystem}`);
// logger.info(`CONFIG notifySystem: ${configFeatures.notifySystem}`);
logger.info("---------------------");
const args = [
configFeatures.verboseLogs ? "--verbose" : null, // Verbose Logs
"--cookies-from-browser", `${getUserBrowser()}`,
"--no-continue",
"--no-overwrites",
"--embed-thumbnail",
"--add-metadata",
"--newline", // YT-DLP Logs Format
configFeatures.addThumbnail ? "--embed-thumbnail" : null,
configFeatures.addMetadata ? "--add-metadata" : null,
"--concurrent-fragments", "8",
"--retries", "10",
"--fragment-retries", "10",
"--ffmpeg-location", ffmpegPath,
"--extractor-args","youtube:player_client=default",
"--js-runtimes", `deno:${denoPath}`
"--js-runtimes", `deno:${denoPath}`,
"-S",
`vcodec:${validateCodec(configFeatures.customCodec) || "h264"}`+
`,acodec:aac`,
configFeatures.autoDownloadPlaylist ? "--yes-playlist" : "--no-playlist"
];
if (audioOnly) args.push("-f", "bestaudio", "--extract-audio", "--audio-format", "mp3");
@@ -36,7 +78,7 @@ function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
args.push("-o", path.join(outputFolder, "%(title)s.%(ext)s"));
args.push(url);
return args;
return args.filter(Boolean);
}
module.exports = { buildYtDlpArgs };

View File

@@ -3,24 +3,31 @@ const fs = require("fs");
const { app } = require("electron");
const config = require("../../config");
const { logger } = require("../logger.js");
let userYtDlp;
let ffmpegPath;
let denoPath;
const sourceYtDlp = path.join(process.resourcesPath, "yt-dlp.exe");
const sourceYtDlp = path.join(process.resourcesPath, "binaries","yt-dlp.exe");
if (config.localMode) {
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe");
ffmpegPath = path.join(__dirname, "../../ressources"); // <- contient ffmpeg.exe et ffprobe.exe
ffmpegPath = path.join(__dirname, "../../ressources/"); // <- contient ffmpeg.exe et ffprobe.exe
denoPath = path.join(__dirname, "../../ressources/deno.exe");
} else {
userYtDlp = path.join(app.getPath("userData"),"yt-dlp.exe");
ffmpegPath = path.join(process.resourcesPath, "ffmpeg.exe");
denoPath = path.join(process.resourcesPath, "deno.exe");
ffmpegPath = path.join(process.resourcesPath, "binaries","ffmpeg.exe");
denoPath = path.join(process.resourcesPath, "binaries","deno.exe");
if (!fs.existsSync(userYtDlp)) {
fs.copyFileSync(sourceYtDlp, userYtDlp);
}
}
if (!userYtDlp){ logger.error("Missing YT-DLP")}
if (!ffmpegPath){ logger.error("Missing FFMPEG")}
if (!denoPath){ logger.error("Missing DENO")}
module.exports = { userYtDlp, ffmpegPath, denoPath };

View File

@@ -0,0 +1,14 @@
const RateLimit = require("express-rate-limit");
/* Rate Limite */
const rateLimite = RateLimit({
windowMs: 15 * 60, // 15 minutes
max: 5, // limit each IP to 100 requests per windowMs on the root path
message: "Too many requests, please try again later.",
statusCode: 429, // HTTP status code for "Too Many Requests"
});
module.exports = {
rateLimite
}

View File

@@ -1,11 +1,11 @@
const express = require("express");
const fs = require("fs");
const path = require("path");
const { logger, logSessionStart, logSessionEnd } = require("./logger");
const { logger, logSessionEnd } = require("./logger");
const config = require("../config");
const { execFile } = require("child_process");
const { userYtDlp } = require("./helpers/path");
const { rateLimite } = require("./helpers/rateLimit")
const app = express();
@@ -37,7 +37,7 @@ app.use(express.static(path.join(__dirname, "../public")));
app.use("/download", require("./routes/download.route"));
app.use("/info", require("./routes/info.route"));
app.get("/", (req, res) => {
app.get("/", rateLimite ,(req, res) => {
res.sendFile(path.join(__dirname, "../public/index.html"));
});

View File

@@ -1,5 +1,5 @@
const { autoUpdater } = require("electron-updater");
const { app, Notification } = require("electron");
const { app, Notification, shell} = require("electron");
const { logger } = require("./logger");
function AutoUpdater() {