diff --git a/.github/PULL_REQUEST_TEMPLATE/release.md b/.github/PULL_REQUEST_TEMPLATE/release.md index c0a1b81..225f7ee 100644 --- a/.github/PULL_REQUEST_TEMPLATE/release.md +++ b/.github/PULL_REQUEST_TEMPLATE/release.md @@ -7,4 +7,4 @@ ## Found a bug or issue? - Please report it in the GitHub Issues section -**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/1.4.0...1.4.1 \ No newline at end of file +**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/1.W.X...1.Y.Z \ No newline at end of file diff --git a/README.md b/README.md index b55dea8..5015289 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ### **A clean, open-source multimedia downloader for Windows** -[![Release](https://img.shields.io/badge/Release-1.4.3-blue?style=for-the-badge)](https://github.com/MasterAcnolo/Freedom-Loader/releases) +[![Release](https://img.shields.io/badge/Release-1.4.4-blue?style=for-the-badge)](https://github.com/MasterAcnolo/Freedom-Loader/releases) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-red.svg?style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0) [![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://masteracnolo.github.io/FreedomLoader/) @@ -419,4 +419,4 @@ You are free to use, modify, and redistribute this software under the terms of t [Website](https://masteracnolo.github.io/FreedomLoader/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues) - + \ No newline at end of file diff --git a/main.js b/main.js index c44438b..ed75eb1 100644 --- a/main.js +++ b/main.js @@ -18,7 +18,8 @@ const basePath = config.localMode const configFolderPath = path.join(basePath, "config" ,"config.json"); -const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader"); +// Default download path (centralized, lazy loaded) +let defaultDownloadPath; app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows app.disableHardwareAcceleration(); @@ -30,11 +31,14 @@ 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"); + const deps = [ - { 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") }, + { 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 = ""; @@ -110,36 +114,52 @@ async function createMainWindow() { } function validateDownloadPath(userPath) { - const userHome = os.homedir(); // C:\Users\ - - 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 !"); + const { isSafePath } = require("./server/helpers/validation"); + + // Lazy load default path + if (!defaultDownloadPath) { + const { defaultDownloadFolder } = require("./server/helpers/path"); + defaultDownloadPath = defaultDownloadFolder; } - return resolved; + 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 () => { - try { - const result = await dialog.showOpenDialog({ properties: ["openDirectory"] }); - if (!result.canceled && result.filePaths.length > 0) { - const validatedPath = validateDownloadPath(result.filePaths[0]); - logger.info(`Folder Checked and Valid : ${validatedPath}`); - return validatedPath; - } - return null; - } catch (err) { - logger.error(`An Error Occured when validating folder : ${err.message}`); + 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) => { @@ -147,7 +167,13 @@ ipcMain.handle("validate-download-path", (event, userPath) => { }); -ipcMain.handle("get-default-download-path", () => defaultDownloadPath); +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 @@ -157,6 +183,8 @@ ipcMain.on("set-progress", (event, percent) => { ipcMain.on("window-minimize", () => { if (mainWindow) mainWindow.minimize(); }); + +// Toggle Maximize -> UnMaximize ipcMain.on("window-maximize", () => { if (mainWindow) { if (mainWindow.isMaximized()) { @@ -166,6 +194,7 @@ ipcMain.on("window-maximize", () => { } } }); + ipcMain.on("window-close", () => { if (mainWindow) mainWindow.close(); }); @@ -248,10 +277,11 @@ app.whenReady().then(async () => { }); - configFeatures.discordRPC ? startRPC() : ""; + if (configFeatures.discordRPC) startRPC(); // Discord RPC await createMainWindow(); - configFeatures.autoUpdate ? AutoUpdater(mainWindow) : ""; // Auto Update + + if (configFeatures.autoUpdate) AutoUpdater(mainWindow); // Auto Update } catch (err) { logger.error("Window or Server error :", err); @@ -260,11 +290,12 @@ app.whenReady().then(async () => { }); app.on("window-all-closed", () => { - logger.info("All Window Closed, shuting down app"); - if (process.platform !== "darwin") app.quit(); + logger.info("Shuting Down App..."); + app.quit(); }); -app.on("before-quit", () => { - logSessionEnd() - stopRPC() +app.on("before-quit", async () => { + await stopRPC(); + logger.info("All Services Stopped. Have a nice day!") + logSessionEnd(); }); diff --git a/package-lock.json b/package-lock.json index d089214..a4e13dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "freedom-loader", - "version": "1.4.3", + "version": "1.4.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "freedom-loader", - "version": "1.4.3", + "version": "1.4.4", "dependencies": { "chalk": "^4.1.2", "debug": "^4.4.1", @@ -3990,9 +3990,9 @@ } }, "node_modules/minimatch": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", - "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", + "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { diff --git a/package.json b/package.json index 5f7e2a9..e1d54f1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "freedom-loader", "productName": "Freedom Loader", - "version": "1.4.3", + "version": "1.4.4", "author": "MasterAcnolo", "description": "Freedom Loader", "main": "main.js", diff --git a/public/script/appVersion.js b/public/script/appVersion.js index ad9aaa0..6ad1775 100644 --- a/public/script/appVersion.js +++ b/public/script/appVersion.js @@ -1,5 +1,7 @@ async function versionLabel(){ const appVersion = await window.electronAPI.getVersion(); + + // Write in front the app version for debugging (bottom right) document.getElementById("version-badge").textContent = `v${appVersion}`; }; diff --git a/public/script/custompath.js b/public/script/custompath.js index ccf01f0..e530c1e 100644 --- a/public/script/custompath.js +++ b/public/script/custompath.js @@ -45,18 +45,15 @@ window.addEventListener("DOMContentLoaded", async () => { .getElementById("changePath") .addEventListener("click", async () => { try { - const selectedPath = + // selectDownloadFolder already returns a validated path + const validatedPath = await window.electronAPI.selectDownloadFolder(); - if (!selectedPath) return; // annulé - - // Validation back obligatoire - const validatedPath = - await window.electronAPI.getValidatedDownloadPath(selectedPath); + if (!validatedPath) return; // cancelled await applyPathFromBack(validatedPath); } catch (err) { - alert("Dossier non autorisé."); + alert("This folder is not allowed. Only specific folders (Users, Downloads, Documents) are authorized for downloads."); } }); }); diff --git a/public/script/fetchinfo.js b/public/script/fetchinfo.js index af9c9b7..d4e1a2c 100644 --- a/public/script/fetchinfo.js +++ b/public/script/fetchinfo.js @@ -78,9 +78,27 @@ async function init() { if (data.type === "playlist") { infoDiv.classList.add("playlist-mode"); infoDiv.innerHTML = ` -

Playlist Detected: ${data.title}

-

Video Count: ${data.count}

-

Channel : ${data.channel || "Unknown"}

+
+
+
Detected Playlist
+

${data.title}

+
+ + + + + ${data.count} vidéos + + ${data.channel ? ` + + + + + ${data.channel} + ` : ''} +
+
+
`; @@ -88,39 +106,58 @@ async function init() { data.videos.forEach(v => { const durationStr = v.duration - ? `${Math.floor(v.duration / 60)}m ${(v.duration % 60).toString().padStart(2,"0")}s` : "Inconnue"; + ? `${Math.floor(v.duration / 60)}:${(v.duration % 60).toString().padStart(2,"0")}` : "--:--"; const videoUrl = v.id ? `https://www.youtube.com/watch?v=${v.id}` : v.url; listDiv.innerHTML += ` -
- Thumbnail -

${v.title}

-

Duration : ${durationStr}

-

URL - -

+
+
+ ${v.title} + ${durationStr} +
+
+

${v.title}

+ ${v.uploader ? `

${v.uploader}

` : ''} +
+ + + + + + Ouvrir + + +
+
`; }); // Gestion du bouton copier listDiv.addEventListener("click", (event) => { - if (event.target.classList.contains("copy-btn")) { - const btn = event.target; + if (event.target.classList.contains("copy-btn") || event.target.closest(".copy-btn")) { + const btn = event.target.closest(".copy-btn") || event.target; if (btn.disabled) return; btn.disabled = true; const url = btn.dataset.url; navigator.clipboard.writeText(url) .then(() => { - const original = btn.textContent; + const original = btn.innerHTML; btn.style.opacity = 0; btn.style.transform = "scale(0.7)"; setTimeout(() => { - btn.textContent = "✅"; + btn.innerHTML = ` + + `; btn.style.opacity = 1; btn.style.transform = "scale(1)"; @@ -129,7 +166,7 @@ async function init() { btn.style.transform = "scale(0.7)"; setTimeout(() => { - btn.textContent = original; + btn.innerHTML = original; btn.style.opacity = 1; btn.style.transform = "scale(1)"; btn.disabled = false; @@ -140,10 +177,12 @@ async function init() { }, 300); }) .catch(() => { - const original = btn.textContent; - btn.textContent = "❌"; + const original = btn.innerHTML; + btn.innerHTML = ` + + `; setTimeout(() => { - btn.textContent = original; + btn.innerHTML = original; btn.disabled = false; }, 1500); }); diff --git a/public/styles/layout/videoinfo.css b/public/styles/layout/videoinfo.css index 743dc00..79ecb98 100644 --- a/public/styles/layout/videoinfo.css +++ b/public/styles/layout/videoinfo.css @@ -69,80 +69,293 @@ /* --- Playlist Mode --- */ #videoInfo.playlist-mode { - max-width: 900px; + max-width: 95%; } -/* Style du bloc "Playlist détectée" */ -#videoInfo.playlist-mode > p { - text-align: center; - font-size: 1.1rem; +/* Playlist Header */ +.playlist-header { + display: flex; + flex-direction: column; + padding: 2rem; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03)); + border-radius: 16px; + margin-bottom: 2rem; + border: 1px solid rgba(255, 255, 255, 0.12); + backdrop-filter: blur(10px); +} + +.playlist-badge { + display: inline-flex; + align-items: center; + width: fit-content; + padding: 0.4rem 0.9rem; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 6px; + font-size: 0.8rem; font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--video-info-text-color); + margin-bottom: 1rem; +} + +.playlist-info { + flex: 1; +} + +.playlist-title { + font-size: 1.9rem; + margin: 0 0 0.6rem 0; + color: var(--video-info-heading-color); + font-weight: 700; + line-height: 1.2; +} + +.playlist-meta { + display: flex; + gap: 1.5rem; + font-size: 0.95rem; + color: var(--video-info-text-color); + opacity: 0.85; + flex-wrap: wrap; +} + +.playlist-count, +.playlist-channel { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0.8rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.08); + font-weight: 500; +} + +.playlist-count svg, +.playlist-channel svg { + width: 16px; + height: 16px; + flex-shrink: 0; } /* Layout liste des vidéos */ #playlistVideos { margin-top: 1em; display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 18px; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.5rem; } -/* Un item de playlist */ -#playlistVideos > div { +/* Carte vidéo playlist */ +.playlist-video-card { background: var(--playlist-background-color); - border-radius: 10px; - padding: 10px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - text-align: center; - transition: transform 0.2s ease; -} - -#playlistVideos > div p { + border-radius: 14px; + overflow: hidden; + box-shadow: 0 2px 12px rgba(0,0,0,0.12); + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; + flex-direction: column; + border: 1px solid rgba(255, 255, 255, 0.1); + position: relative; } -#videoInfo.playlist-mode { - max-width: 95%; +.playlist-video-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, var(--video-info-link-color), transparent); + opacity: 0; + transition: opacity 0.25s ease; } -#playlistVideos > div:hover { +.playlist-video-card:hover { + transform: translateY(-6px); + box-shadow: 0 12px 32px rgba(0,0,0,0.25); + border-color: rgba(255, 255, 255, 0.15); +} + +.playlist-video-card:hover::before { + opacity: 1; +} + +/* Thumbnail wrapper avec durée */ +.video-thumbnail-wrapper { + position: relative; + width: 100%; + aspect-ratio: 16 / 9; + overflow: hidden; + background: #000; +} + +.video-thumbnail { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + transition: transform 0.3s ease; +} + +.playlist-video-card:hover .video-thumbnail { transform: scale(1.05); } -#playlistVideos img { - width: 100%; +.video-duration { + position: absolute; + bottom: 8px; + right: 8px; + background: rgba(0, 0, 0, 0.9); + color: white; + padding: 4px 8px; + border-radius: 6px; + font-size: 0.85rem; + font-weight: 700; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace; + letter-spacing: 0.5px; + backdrop-filter: blur(4px); +} + +/* Contenu vidéo */ +.video-content { + padding: 1.2rem; + display: flex; + flex-direction: column; + gap: 0.6rem; + flex: 1; + background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.02)); +} + +.video-title { + font-size: 1rem; + font-weight: 600; + margin: 0; + color: var(--video-info-heading-color); + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + min-height: 2.8em; + transition: color 0.2s ease; +} + +.playlist-video-card:hover .video-title { + color: var(--video-info-link-color); +} + +.video-uploader { + font-size: 0.9rem; + color: var(--video-info-text-color); + opacity: 0.7; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Actions vidéo */ +.video-actions { + display: flex; + align-items: center; + gap: 0.6rem; + margin-top: auto; + padding-top: 0.8rem; + border-top: 1px solid rgba(255, 255, 255, 0.08); +} + +.video-link { + flex: 1; + text-decoration: none; + color: var(--video-info-link-color); + font-size: 0.9rem; + padding: 0.5rem 0.9rem; border-radius: 8px; - margin-bottom: 8px; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.08); + transition: all 0.2s ease; + text-align: center; + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + gap: 0.4rem; +} + +.video-link svg { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.video-link:hover { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.15); + text-decoration: none; + transform: translateY(-1px); } .copy-btn { - all: unset; - background: transparent; - border: none; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.08); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; - padding: 4px; - vertical-align: middle; - transition: transform 0.1s ease, color 0.1s ease, opacity 0.15s ease; - font-size: 1em; + padding: 0.5rem; + border-radius: 8px; + transition: all 0.2s ease; + width: 40px; + height: 40px; + flex-shrink: 0; } -#playlistVideos > div p a{ - text-decoration: none; - color: var(--video-info-link-color); +.copy-btn svg { + width: 18px; + height: 18px; + color: var(--video-info-text-color); } -#playlistVideos > div p a:hover{ - text-decoration: underline; +.copy-btn:hover { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.15); + transform: translateY(-1px); +} + +.copy-btn:active { + transform: scale(0.95); +} + +.copy-btn:disabled { + cursor: not-allowed; + opacity: 0.6; } /* Responsive */ +@media (max-width: 768px) { + #playlistVideos { + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 1rem; + } + + .playlist-header { + padding: 1.5rem; + } + + .playlist-title { + font-size: 1.5rem; + } + + .playlist-meta { + gap: 0.8rem; + } +} + @media (max-width: 480px) { #videoInfo { padding: 1em; @@ -152,4 +365,21 @@ #videoInfo h3 { font-size: 1.4rem; } + + #playlistVideos { + grid-template-columns: 1fr; + } + + .playlist-header { + padding: 1.2rem; + } + + .playlist-title { + font-size: 1.3rem; + } + + .playlist-badge { + font-size: 0.75rem; + padding: 0.4rem 0.8rem; + } } diff --git a/server/controller/download.controller.js b/server/controller/download.controller.js index 597b290..f875859 100644 --- a/server/controller/download.controller.js +++ b/server/controller/download.controller.js @@ -16,10 +16,14 @@ async function downloadController(req, res) { }; if (!options.url || !isValidUrl(options.url)) return res.status(400).send("❌ Invalid URL !"); - if (options.outputFolder && !isSafePath(options.outputFolder)) return res.status(400).send("❌ Save Path Not Allowed."); + if (options.outputFolder && !isSafePath(options.outputFolder)) { + logger.warn(`Unsafe download path rejected: ${options.outputFolder}`); + return res.status(400).send("❌ Save Path Not Allowed."); + } - const filePath = await fetchDownload(options, listeners, speedListeners); - notifyDownloadFinished(filePath); + // Get output folder when the download is finished + const outputFolder = await fetchDownload(options, listeners, speedListeners); + notifyDownloadFinished(outputFolder); res.send("✅ Download Done !"); } catch (err) { diff --git a/server/controller/info.controller.js b/server/controller/info.controller.js index 7002cbf..81837a9 100644 --- a/server/controller/info.controller.js +++ b/server/controller/info.controller.js @@ -13,7 +13,7 @@ async function infoController(req, res){ logger.info(`/Info Request receive by the Info Controller. URL: ${url}`); - if (url.includes("?list")) { + if (url.includes("&list") || url.includes("?list")) { logger.info("Estimated Data Type: Playlist") } else{ diff --git a/server/helpers/getBrowser.js b/server/helpers/getBrowser.js index f51c8f2..a362a55 100644 --- a/server/helpers/getBrowser.js +++ b/server/helpers/getBrowser.js @@ -2,6 +2,7 @@ const fs = require("fs"); const os = require("os"); const path = require("path"); const notify = require("./notify") +const { logger } = require("../logger"); function getUserBrowser() { const userProfile = os.homedir(); @@ -18,15 +19,21 @@ function getUserBrowser() { // { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") } ]; + // Chercher un navigateur disponible for (const browser of browsers) { if (fs.existsSync(browser.path)) { + logger.info(`Browser found: ${browser.name}`); return browser.name; - } else{ - notify.notifyFirefoxBrowserMissing(); } } - notify.notifyCookiesBrowserError() - return ; + + // Aucun navigateur trouvé - notifier l'utilisateur + logger.warn("No supported browser found on the system"); + notify.notifyFirefoxBrowserMissing(); + + // Fallback: retourner "firefox" pour laisser yt-dlp gérer l'erreur + // plutôt que de crasher l'application + return "firefox"; } module.exports = getUserBrowser \ No newline at end of file diff --git a/server/helpers/notify.js b/server/helpers/notify.js index f9a24b7..026cc81 100644 --- a/server/helpers/notify.js +++ b/server/helpers/notify.js @@ -1,12 +1,11 @@ const { Notification, shell } = require("electron"); -const path = require("path"); +const { iconPaths } = require("./path"); function notifyDownloadFinished(folder) { - const iconPath = path.join(process.resourcesPath, "confirm-icon.png"); const notif = new Notification({ title: "Freedom Loader", body: "Your download is complete, click here to open it.", - icon: iconPath, + icon: iconPaths.confirm, }); notif.on("click", () => shell.openPath(folder)); @@ -14,11 +13,10 @@ function notifyDownloadFinished(folder) { } function notifyCookiesBrowserError(){ - const iconPath = path.join(process.resourcesPath, "error.png"); const notif = new Notification({ title: "Cookies Error", body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.", - icon: iconPath, + icon: iconPaths.error, }); notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1")); @@ -26,11 +24,10 @@ function notifyCookiesBrowserError(){ } function notifyFirefoxBrowserMissing(){ - const iconPath = path.join(process.ressourcesPath, "error.png"); const notif = new Notification({ title: "Firefox Missing", body: "Firefox was not found on your system. Click here to follow the installation guide", - icon: iconPath, + icon: iconPaths.error, }); notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1")) diff --git a/server/helpers/parseInfo.js b/server/helpers/parseInfo.js index c397493..6449061 100644 --- a/server/helpers/parseInfo.js +++ b/server/helpers/parseInfo.js @@ -2,12 +2,6 @@ function parseVideo(data) { return { type: "video", - // id: data.id, - // title: data.title, - // url: data.webpage_url, - // duration: data.duration, - // thumbnail: data.thumbnail, - // uploader: data.uploader ...data }; } diff --git a/server/helpers/path.js b/server/helpers/path.js index 8eac1a5..2a7d823 100644 --- a/server/helpers/path.js +++ b/server/helpers/path.js @@ -1,15 +1,25 @@ const path = require("path"); const fs = require("fs"); +const os = require("os"); const { app } = require("electron"); const config = require("../../config"); const { logger } = require("../logger.js"); +// Centralized resource paths +const resourcesPath = config.localMode + ? path.join(__dirname, "../../ressources") + : process.resourcesPath; + +// Default download folder (centralized) +const defaultDownloadFolder = path.join(os.homedir(), "Downloads", "Freedom Loader"); + +// Binary paths let userYtDlp; let ffmpegPath; let denoPath; -const sourceYtDlp = path.join(process.resourcesPath, "binaries","yt-dlp.exe"); +const sourceYtDlp = path.join(resourcesPath, "binaries","yt-dlp.exe"); if (config.localMode) { userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe"); @@ -17,8 +27,8 @@ if (config.localMode) { denoPath = path.join(__dirname, "../../ressources/deno.exe"); } else { userYtDlp = path.join(app.getPath("userData"),"yt-dlp.exe"); - ffmpegPath = path.join(process.resourcesPath, "binaries","ffmpeg.exe"); - denoPath = path.join(process.resourcesPath, "binaries","deno.exe"); + ffmpegPath = path.join(resourcesPath, "binaries","ffmpeg.exe"); + denoPath = path.join(resourcesPath, "binaries","deno.exe"); if (!fs.existsSync(userYtDlp)) { fs.copyFileSync(sourceYtDlp, userYtDlp); @@ -26,8 +36,23 @@ if (config.localMode) { } +// Notification icon paths +const iconPaths = { + confirm: path.join(resourcesPath, "confirm-icon.png"), + error: path.join(resourcesPath, "error.png"), + app: path.join(resourcesPath, "app-icon.ico") +}; + +// Binary paths for verification +const binaryPaths = { + ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"), + ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"), + ffprobe: path.join(resourcesPath, "binaries", "ffprobe.exe"), + deno: path.join(resourcesPath, "binaries", "deno.exe") +}; + if (!userYtDlp){ logger.error("Missing YT-DLP")} if (!ffmpegPath){ logger.error("Missing FFMPEG")} if (!denoPath){ logger.error("Missing DENO")} -module.exports = { userYtDlp, ffmpegPath, denoPath }; +module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder }; diff --git a/server/helpers/rateLimit.js b/server/helpers/rateLimit.js index d65336f..8d85534 100644 --- a/server/helpers/rateLimit.js +++ b/server/helpers/rateLimit.js @@ -3,7 +3,7 @@ const RateLimit = require("express-rate-limit"); /* Rate Limite */ const rateLimite = RateLimit({ - windowMs: 15 * 60, // 15 minutes + windowMs: 15 * 60 * 1000, // 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" diff --git a/server/helpers/validation.js b/server/helpers/validation.js index b6ede30..b7e3da1 100644 --- a/server/helpers/validation.js +++ b/server/helpers/validation.js @@ -13,7 +13,7 @@ function isValidUrl(url) { function isSafePath(folder) { if (!folder || folder.length < 3) return false; - const unsafe = ["System32", "/etc", "\\Windows"]; + const unsafe = ["System32", "\\Windows"]; const resolved = path.resolve(folder); return !unsafe.some(u => resolved.includes(u)); } diff --git a/server/logger.js b/server/logger.js index 632d553..9e4274c 100644 --- a/server/logger.js +++ b/server/logger.js @@ -3,13 +3,17 @@ const DailyRotateFile = require("winston-daily-rotate-file"); const fs = require("fs"); const path = require("path"); const os = require("os"); -const config = require("../config") +const config = require("../config"); // Dossier de logs Windows const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs"); // Création du dossier "logs" si nécessaire -fs.mkdirSync(logDir, { recursive: true }); +try { + fs.mkdirSync(logDir, { recursive: true }); +} catch (error) { + console.error(`Failed to create log directory: ${error.message}`); +} const logFormat = format.combine( format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), @@ -36,21 +40,13 @@ const logger = createLogger({ ], }); -function getSessionStartLine() { - return `--- Starting session: ${new Date().toISOString()} ---`; -} - -function getSessionEndLine() { - return `--- Ending session: ${new Date().toISOString()} ---`; -} - function logSessionStart() { - logger.info(getSessionStartLine()); + logger.info(`--- Starting session: ${new Date().toISOString()} ---`); logger.info(`Application Version: ${config.version}`) } function logSessionEnd() { - logger.info(getSessionEndLine()); + logger.info(`--- Ending session: ${new Date().toISOString()} ---`); } module.exports = { diff --git a/server/server.js b/server/server.js index e8f1f75..2f229ed 100644 --- a/server/server.js +++ b/server/server.js @@ -1,5 +1,4 @@ const express = require("express"); -const fs = require("fs"); const path = require("path"); const { logger, logSessionEnd } = require("./logger"); const config = require("../config"); @@ -11,19 +10,7 @@ const app = express(); app.use(express.json()); -// Dossier de téléchargement -const outputFolder = path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader"); - -// Création du dossier si nécessaire -try { - fs.mkdirSync(outputFolder, { recursive: true }); - logger.info(`Freedom Loader folder ready in ${outputFolder}`); -} catch (err) { - logger.error("Unable to create folder:", err); - process.exit(1); -} - -// Mise à jour yt-dlp au +// Mise à jour yt-dlp au démarrage execFile(userYtDlp, ["-U"], (err, stdout, stderr) => { if (err) logger.warn("yt-dlp update error:", err); else logger.info(`Update yt-dlp : ${stdout}`); diff --git a/server/services/download.services.js b/server/services/download.services.js index 789fbd7..d869c4a 100644 --- a/server/services/download.services.js +++ b/server/services/download.services.js @@ -1,18 +1,35 @@ const { execFile } = require("child_process"); -const { userYtDlp } = require("../helpers/path"); -const path = require("path"); +const { userYtDlp, defaultDownloadFolder } = require("../helpers/path"); const fs = require("fs"); const { logger } = require("../logger"); const { buildYtDlpArgs } = require("../helpers/buildArgs"); -const notify = require("../helpers/notify") +const notify = require("../helpers/notify"); +const path = require("path"); +const { isSafePath } = require("../helpers/validation"); function fetchDownload(options, listeners, speedListeners) { return new Promise((resolve, reject) => { - const outputFolder = options.outputFolder || path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader"); - fs.mkdirSync(outputFolder, { recursive: true }); + const outputFolder = options.outputFolder || defaultDownloadFolder; + + // Normalize path and validate it's safe (within Users folder) + let safeOutputFolder = path.resolve(outputFolder); + + if (!isSafePath(safeOutputFolder)) { + logger.warn(`Path not allowed, using default instead: ${safeOutputFolder}`); + safeOutputFolder = path.resolve(defaultDownloadFolder); + } + + // Create download folder if it doesn't exist + try { + fs.mkdirSync(safeOutputFolder, { recursive: true }); + logger.info(`Output folder ready: ${safeOutputFolder}`); + } catch (err) { + logger.error(`Failed to create output folder: ${err.message}`); + return reject(new Error(`Unable to create download folder: ${err.message}`)); + } - const args = buildYtDlpArgs({ ...options, outputFolder }); + const args = buildYtDlpArgs({ ...options, outputFolder: safeOutputFolder }); logger.info(`[yt-dlp args] ${args.join(" ")}`); const child = execFile(userYtDlp, args); @@ -21,7 +38,7 @@ function fetchDownload(options, listeners, speedListeners) { child.on("close", code => { listeners.forEach(fn => fn("done")); - if (code === 0) resolve(outputFolder); + if (code === 0) resolve(safeOutputFolder); else reject(new Error(`YT-DLP failed with code : ${code}`)); }); @@ -31,7 +48,7 @@ function fetchDownload(options, listeners, speedListeners) { if (!line.trim()) return; logger.info(`[yt-dlp] ${line}`); - /* Barre de Chargement*/ + /* Progress Bar */ if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset")); const match = line.match(/\[download\]\s+(\d+\.\d+)%/); if (match) listeners.forEach(fn => fn(parseFloat(match[1])));