From 96401a6e1f77cd47dd97481d1eaf7dd86c32d89b Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:14:21 +0200 Subject: [PATCH] Refactor: split main.js into app/ modules --- server/update.js => app/autoUpdater.js | 2 +- app/dependencyCheck.js | 32 +++ {server => app}/discordRPC.js | 2 +- app/ipcHandlers.js | 111 +++++++++ app/pathValidator.js | 32 +++ app/windowManager.js | 50 ++++ app/ytDlpUpdater.js | 12 + main.js | 305 +++---------------------- server/server.js | 25 +- 9 files changed, 284 insertions(+), 287 deletions(-) rename server/update.js => app/autoUpdater.js (96%) create mode 100644 app/dependencyCheck.js rename {server => app}/discordRPC.js (95%) create mode 100644 app/ipcHandlers.js create mode 100644 app/pathValidator.js create mode 100644 app/windowManager.js create mode 100644 app/ytDlpUpdater.js diff --git a/server/update.js b/app/autoUpdater.js similarity index 96% rename from server/update.js rename to app/autoUpdater.js index d38aa70..0f4a4ac 100644 --- a/server/update.js +++ b/app/autoUpdater.js @@ -1,6 +1,6 @@ const { autoUpdater } = require("electron-updater"); const { app, Notification, shell} = require("electron"); -const { logger } = require("./logger"); +const { logger } = require("../server/logger"); function AutoUpdater() { diff --git a/app/dependencyCheck.js b/app/dependencyCheck.js new file mode 100644 index 0000000..ba285c9 --- /dev/null +++ b/app/dependencyCheck.js @@ -0,0 +1,32 @@ +const fs = require("fs"); +const { app, dialog } = require("electron"); +const { logger } = require("../server/logger"); + +function checkNativeDependencies() { + const { binaryPaths } = require("../server/helpers/path.helpers"); + + const deps = [ + { name: "yt-dlp.exe", path: binaryPaths.ytDlp }, + { name: "ffmpeg.exe", path: binaryPaths.ffmpeg }, + { name: "ffprobe.exe", path: binaryPaths.ffprobe }, + { name: "deno.exe", path: binaryPaths.deno }, + ]; + + const missing = deps.filter(d => !fs.existsSync(d.path)); + if (missing.length === 0) return true; + + const list = missing.map(d => d.name).join(", "); + logger.error(`Missing dependencies: ${list}`); + + app.whenReady().then(() => { + dialog.showErrorBox( + "Missing dependencies", + `The following files are missing in the 'ressources' folder:\n${list}\n\nThe application will now exit. Try to reinstall.` + ); + app.quit(); + }); + + return false; +} + +module.exports = { checkNativeDependencies }; \ No newline at end of file diff --git a/server/discordRPC.js b/app/discordRPC.js similarity index 95% rename from server/discordRPC.js rename to app/discordRPC.js index 90c96d4..49b5416 100644 --- a/server/discordRPC.js +++ b/app/discordRPC.js @@ -1,6 +1,6 @@ const config = require('../config'); const RPC = require("discord-rpc"); -const { logger } = require("./logger"); +const { logger } = require("../server/logger"); const clientId = `${config.DiscordRPCID}`; const rpc = new RPC.Client({ transport: "ipc" }); diff --git a/app/ipcHandlers.js b/app/ipcHandlers.js new file mode 100644 index 0000000..8e953a0 --- /dev/null +++ b/app/ipcHandlers.js @@ -0,0 +1,111 @@ +const { ipcMain, dialog, shell } = require("electron"); +const fs = require("fs"); +const path = require("path"); +const { logger, logDir } = require("../server/logger"); +const { configFeatures } = require("../config"); +const config = require("../config"); +const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator"); + +const configFolderPath = path.join( + config.localMode + + ? path.join(__dirname, "..") + : path.join(path.dirname(process.execPath), "resources"), "config", "config.json" +); + +const FEATURE_WHITELIST = new Set([ + "autoUpdate", + "discordRPC", + "customTopBar", + "autoCheckInfo", + "addThumbnail", + "addMetadata", + "verboseLogs", + "autoDownloadPlaylist", + "customCodec", +]); + +function registerIpcHandlers(getMainWindow) { + + // Infos générales + ipcMain.handle("version", () => config.version); + ipcMain.handle("features", () => configFeatures); + + // Sélection et validation de dossier + ipcMain.handle("select-download-folder", async () => { + const result = await dialog.showOpenDialog({ properties: ["openDirectory"] }); + if (result.canceled) { + logger.info("Folder selection cancelled by user"); + return null; + } + try { + const validated = validateDownloadPath(result.filePaths[0]); + logger.info(`Folder selected and validated: ${validated}`); + return validated; + } catch (err) { + logger.warn(`Unsafe or invalid folder rejected: ${err.message}`); + throw err; + } + }); + + ipcMain.handle("validate-download-path", (_, userPath) => validateDownloadPath(userPath)); + ipcMain.handle("get-default-download-path", () => getDefaultDownloadPath()); + + // Progression dans la taskbar + ipcMain.on("set-progress", (_, percent) => { + getMainWindow()?.setProgressBar(percent / 100); + }); + + // Contrôles fenêtre (custom top bar) + ipcMain.on("window-minimize", () => getMainWindow()?.minimize()); + ipcMain.on("window-maximize", () => { + const win = getMainWindow(); + if (!win) return; + win.isMaximized() ? win.unmaximize() : win.maximize(); + }); + ipcMain.on("window-close", () => getMainWindow()?.close()); + + // Actions custom + ipcMain.on("open-devtools", () => + getMainWindow()?.webContents.openDevTools({ mode: "detach" }) + ); + ipcMain.on("open-logs", () => logDir && shell.openPath(logDir)); + ipcMain.on("open-website", () => + shell.openExternal("https://masteracnolo.github.io/FreedomLoader/index.html") + ); + ipcMain.on("open-wiki", () => + shell.openExternal("https://masteracnolo.github.io/FreedomLoader/pages/wiki.html") + ); + ipcMain.on("open-config", () => shell.openPath(configFolderPath)); + + // Modification des features + ipcMain.handle("set-feature", (event, { key, value }) => { + try { + if (!FEATURE_WHITELIST.has(key)) { + logger.warn(`Rejected feature (not whitelisted): ${key}`); + return false; + } + + 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; + } + }); +} + +module.exports = { registerIpcHandlers }; \ No newline at end of file diff --git a/app/pathValidator.js b/app/pathValidator.js new file mode 100644 index 0000000..aa69107 --- /dev/null +++ b/app/pathValidator.js @@ -0,0 +1,32 @@ +const fs = require("fs"); +const path = require("path"); +const { logger } = require("../server/logger"); + +let _defaultPath = null; + +function getDefaultDownloadPath() { + if (!_defaultPath) { + const { defaultDownloadFolder } = require("../server/helpers/path.helpers"); + _defaultPath = defaultDownloadFolder; + } + return _defaultPath; +} + +function validateDownloadPath(userPath) { + const { isSafePath } = require("../server/helpers/validation.helpers"); + + if (!userPath) return getDefaultDownloadPath(); + + try { + const resolved = fs.realpathSync(path.resolve(userPath)); + if (!isSafePath(resolved)) { + throw new Error("Path not allowed: system folders are blocked!"); + } + return resolved; + } catch (err) { + logger.error(`Invalid download path: ${userPath} — ${err.message}`); + throw new Error(`Invalid or inaccessible path: ${err.message}`); + } +} + +module.exports = { validateDownloadPath, getDefaultDownloadPath }; \ No newline at end of file diff --git a/app/windowManager.js b/app/windowManager.js new file mode 100644 index 0000000..4b4c273 --- /dev/null +++ b/app/windowManager.js @@ -0,0 +1,50 @@ +const { BrowserWindow, app } = require("electron"); +const path = require("path"); +const { logger } = require("../server/logger"); +const { configFeatures } = require("../config"); +const config = require("../config"); + +let mainWindow = null; + +async function createMainWindow() { + if (mainWindow) { + logger.warn("Window already exists, no new creation!"); + return mainWindow; + } + + mainWindow = new BrowserWindow({ + title: `Freedom Loader ${config.version}`, + width: 750, + height: 800, + minWidth: 750, + minHeight: 800, + frame: !configFeatures.customTopBar, + devTools: !app.isPackaged, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, "../preload.js"), + }, + }); + + try { + await mainWindow.loadURL(`http://localhost:${config.applicationPort}`); + logger.info("Window loaded"); + } catch (err) { + logger.error("Window loading error:", err); + throw err; + } + + mainWindow.on("closed", () => { + logger.info("Main window closed"); + mainWindow = null; + }); + + return mainWindow; +} + +function getMainWindow() { + return mainWindow; +} + +module.exports = { createMainWindow, getMainWindow }; \ No newline at end of file diff --git a/app/ytDlpUpdater.js b/app/ytDlpUpdater.js new file mode 100644 index 0000000..90b400b --- /dev/null +++ b/app/ytDlpUpdater.js @@ -0,0 +1,12 @@ +const { execFile } = require("child_process"); +const { logger } = require("../server/logger"); + +function updateYtDlp(ytDlpPath) { + logger.info("yt-dlp update check starting..."); + execFile(ytDlpPath, ["-U"], (err, stdout) => { + if (err) logger.warn("yt-dlp update failed (continuing):", err.message); + else logger.info(`yt-dlp update: ${stdout.trim()}`); + }); +} + +module.exports = { updateYtDlp }; \ No newline at end of file diff --git a/main.js b/main.js index 58621c9..6041f89 100644 --- a/main.js +++ b/main.js @@ -1,301 +1,72 @@ -const config = require("./config.js"); -const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron"); -const path = require("path"); -const fs = require("fs"); +process.on("uncaughtException", (err) => { + console.error("Uncaught exception:", err); + app.quit(); +}); -const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger"); -const { AutoUpdater } = require("./server/update.js"); -const { configFeatures } = require("./config.js"); -const { startRPC, stopRPC} = require("./server/discordRPC"); +process.on("unhandledRejection", (reason) => { + console.error("Unhandled rejection:", reason); + app.quit(); +}); -let mainWindow; -const logsFolderPath = logDir; +const { app } = require("electron"); -const basePath = config.localMode - ? path.join(__dirname ) - : path.join(path.dirname(process.execPath), "resources"); +const { logger, logSessionStart, logSessionEnd } = require("./server/logger"); +const { AutoUpdater } = require("./app/autoUpdater"); +const { startRPC, stopRPC } = require("./app/discordRPC"); -const configFolderPath = path.join(basePath, "config" ,"config.json"); +const { configFeatures } = require("./config"); +const config = require("./config"); -// Default download path (centralized, lazy loaded) -let defaultDownloadPath; +const { checkNativeDependencies } = require("./app/dependencyCheck"); +const { updateYtDlp } = require("./app/ytDlpUpdater"); +const { createMainWindow, getMainWindow } = require("./app/windowManager"); +const { registerIpcHandlers } = require("./app/ipcHandlers"); -app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows +app.setAppUserModelId("com.masteracnolo.freedomloader"); app.disableHardwareAcceleration(); -ipcMain.handle("version", () => config.version); - -// Gestion single instance -const gotLock = app.requestSingleInstanceLock(); - -// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno) -function checkNativeDependencies() { - // Import centralized paths after app initialization - const { binaryPaths } = require("./server/helpers/path.helpers.js"); - - const deps = [ - { name: "yt-dlp.exe", path: binaryPaths.ytDlp }, - { name: "ffmpeg.exe", path: binaryPaths.ffmpeg }, - { name: "ffprobe.exe", path: binaryPaths.ffprobe }, - { name: "deno.exe", path: binaryPaths.deno }, - ]; - const missing = deps.filter(dep => !fs.existsSync(dep.path)); - let errorMsg = ""; - if (missing.length > 0) { - const missingList = missing.map(dep => dep.name).join(", "); - logger.error(`Missing dependencies: ${missingList}`); - errorMsg += `The following files are missing in the 'ressources' folder:\n${missingList}`; - } - if (errorMsg) { - app.whenReady().then(() => { - dialog.showErrorBox( - "Missing dependencies", - `${errorMsg}\n\nThe application will now exit. Try to reinstall` - ); - app.quit(); +if (!config.localMode) { + const gotLock = app.requestSingleInstanceLock(); + if (gotLock) { + app.on("second-instance", () => { + logger.info("New instance detected, closing older..."); + getMainWindow()?.destroy(); }); - return false; - } - return true; -} - -if(!config.localMode){ - if (!gotLock) { - // Une instance existe déjà -> fermer l'ancienne et continuer la nouvelle - // Ici la nouvelle instance continue normalement - } else { - if (!checkNativeDependencies()) { - // Arrêt déjà géré dans la fonction - } else { - app.on("second-instance", () => { - // La vieille instance se ferme - if (mainWindow) { - logger.info("New Instance Detected, closing the older..."); - mainWindow.destroy(); - mainWindow = null; - } - }); - } - } -} -// Création fenêtre principale -async function createMainWindow() { - if (mainWindow) { - logger.warn("Window already exists, no new creation!"); - return; - } - - mainWindow = new BrowserWindow({ - title: `Freedom Loader ${config.version}`, - width: 750, - height: 800, - minWidth: 750, - minHeight: 800, - frame: !configFeatures.customTopBar, - devTools: `${app.isPackaged ? false : true}`, - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - preload: path.join(__dirname, "preload.js"), - }, - }); - - try { - await mainWindow.loadURL(`http://localhost:${config.applicationPort}`); - logger.info("Window Loaded"); - } catch (err) { - logger.error("Window Loading Error:", err); - } - - mainWindow.on("closed", () => { - logger.info("Main Window Closed"); - mainWindow = null; - }); -} - -function validateDownloadPath(userPath) { - const { isSafePath } = require("./server/helpers/validation.helpers.js"); - - // Lazy load default path - if (!defaultDownloadPath) { - const { defaultDownloadFolder } = require("./server/helpers/path.helpers.js"); - defaultDownloadPath = defaultDownloadFolder; - } - - if (!userPath) return defaultDownloadPath; - - try { - // Canonical resolution and symlink following - const resolved = fs.realpathSync(path.resolve(userPath)); - - // Use the same validation as backend (allows all drives except system folders) - if (!isSafePath(resolved)) { - throw new Error("Path not allowed: system folders are blocked!"); - } - - return resolved; - } catch (err) { - logger.error(`Invalid download path: ${userPath} - ${err.message}`); - throw new Error(`Invalid or inaccessible path: ${err.message}`); } } - -// IPC -ipcMain.handle("select-download-folder", async () => { - const result = await dialog.showOpenDialog({ properties: ["openDirectory"] }); - if (result.canceled) { - logger.info("Folder selection cancelled by user"); - return null; - } - if (result.filePaths.length > 0) { - const selectedPath = result.filePaths[0]; - try { - const validatedPath = validateDownloadPath(selectedPath); - logger.info(`Folder selected and validated: ${validatedPath}`); - return validatedPath; - } catch (err) { - logger.warn(`Unsafe or invalid folder rejected: ${err.message}`); - throw err; // Propagate error to UI - } - } - return null; -}); - -ipcMain.handle("validate-download-path", (event, userPath) => { - return validateDownloadPath(userPath); -}); - - -ipcMain.handle("get-default-download-path", () => { - if (!defaultDownloadPath) { - const { defaultDownloadFolder } = require("./server/helpers/path"); - defaultDownloadPath = defaultDownloadFolder; - } - return defaultDownloadPath; -}); - -ipcMain.on("set-progress", (event, percent) => { - if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1 -}); - -// Topbar window controls -ipcMain.on("window-minimize", () => { - if (mainWindow) mainWindow.minimize(); -}); - -// Toggle Maximize -> UnMaximize -ipcMain.on("window-maximize", () => { - if (mainWindow) { - if (mainWindow.isMaximized()) { - mainWindow.unmaximize(); - } else { - mainWindow.maximize(); - } - } -}); - -ipcMain.on("window-close", () => { - if (mainWindow) mainWindow.close(); -}); - -// Topbar custom actions -ipcMain.on("open-devtools", () => { - if (mainWindow) mainWindow.webContents.openDevTools({ mode: 'detach' }); -}); -ipcMain.on("open-logs", () => { - if (logsFolderPath) shell.openPath(logsFolderPath); -}); -ipcMain.on("open-website", () => { - shell.openExternal("https://masteracnolo.github.io/FreedomLoader/index.html"); -}); -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 app.whenReady().then(async () => { logSessionStart(); - logger.info("App Ready, Server Express starting..."); - const serverPath = path.join(__dirname, "server", "server.js") + if (!config.localMode && !checkNativeDependencies()) return; - const expressServer = require(serverPath); + const { userYtDlp } = require("./server/helpers/path.helpers"); + updateYtDlp(userYtDlp); try { - await expressServer.startServer(); - logger.info("Express Server Started"); + await require("./server/server").startServer(); - 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; - } - }); - - - if (configFeatures.discordRPC) startRPC(); // Discord RPC + registerIpcHandlers(getMainWindow); await createMainWindow(); - if (configFeatures.autoUpdate) AutoUpdater(mainWindow); // Auto Update + if (configFeatures.discordRPC) startRPC(); + + if (configFeatures.autoUpdate) AutoUpdater(getMainWindow()); } catch (err) { - logger.error("Window or Server error :", err); + logger.error("Boot error:", err); app.quit(); } }); app.on("window-all-closed", () => { - logger.info("Shuting Down App..."); + logger.info("Shutting down..."); app.quit(); }); app.on("before-quit", async () => { await stopRPC(); - logger.info("All Services Stopped. Have a nice day!") - logSessionEnd(); -}); + logger.info("All services stopped. Have a nice day!"); + logSessionEnd(); +}); \ No newline at end of file diff --git a/server/server.js b/server/server.js index 3992fd9..a31e6ec 100644 --- a/server/server.js +++ b/server/server.js @@ -2,30 +2,19 @@ const express = require("express"); const path = require("path"); const { logger, logSessionEnd } = require("./logger"); const config = require("../config"); -const { execFile } = require("child_process"); -const { userYtDlp } = require("./helpers/path.helpers"); -const { rateLimite } = require("./helpers/rateLimit.helpers") +const { rateLimite } = require("./helpers/rateLimit.helpers"); const app = express(); -app.use(express.json()); - -// Update YT-DLP on Startup -execFile(userYtDlp, ["-U"], (err, stdout, stderr) => { - if (err) logger.warn("yt-dlp update error:", err); - else logger.info(`Update yt-dlp : ${stdout}`); -}); - // Middlewares +app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname, "../public"))); // Routes app.use("/download", require("./routes/download.route")); -app.use("/info", require("./routes/info.route")); - -// When we get API Root, it return the front pages. -app.get("/", rateLimite ,(req, res) => { +app.use("/info", require("./routes/info.route")); +app.get("/", rateLimite, (req, res) => { res.sendFile(path.join(__dirname, "../public/index.html")); }); @@ -37,19 +26,19 @@ async function startServer() { }); server.on("error", (err) => { - logger.error("Express Server Error :", err); + logger.error("Express server error:", err); reject(err); }); const gracefulExit = () => { logSessionEnd(); server.close(() => { - logger.info("Express Server close cleanly."); + logger.info("Express server closed cleanly."); process.exit(); }); }; - process.on("SIGINT", gracefulExit); + process.on("SIGINT", gracefulExit); process.on("SIGTERM", gracefulExit); }); }