Merge with false positive

Merge with False positive
This commit is contained in:
MasterAcnolo
2026-02-18 15:10:01 +01:00
committed by GitHub
20 changed files with 494 additions and 168 deletions

View File

@@ -7,4 +7,4 @@
## Found a bug or issue? ## Found a bug or issue?
- Please report it in the GitHub Issues section - Please report it in the GitHub Issues section
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/1.4.0...1.4.1 **Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/1.W.X...1.Y.Z

View File

@@ -7,7 +7,7 @@
### **A clean, open-source multimedia downloader for Windows** ### **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) [![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/) [![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://masteracnolo.github.io/FreedomLoader/)
<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> <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>
@@ -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) [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)
</div> </div>

99
main.js
View File

@@ -18,7 +18,8 @@ const basePath = config.localMode
const configFolderPath = path.join(basePath, "config" ,"config.json"); 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.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
@@ -30,11 +31,14 @@ const gotLock = app.requestSingleInstanceLock();
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno) // Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
function checkNativeDependencies() { function checkNativeDependencies() {
// Import centralized paths after app initialization
const { binaryPaths } = require("./server/helpers/path");
const deps = [ const deps = [
{ name: "yt-dlp.exe", path: path.join(process.resourcesPath, "binaries","yt-dlp.exe") }, { name: "yt-dlp.exe", path: binaryPaths.ytDlp },
{ name: "ffmpeg.exe", path: path.join(process.resourcesPath, "binaries", "ffmpeg.exe") }, { name: "ffmpeg.exe", path: binaryPaths.ffmpeg },
{ name: "ffprobe.exe", path: path.join(process.resourcesPath, "binaries", "ffprobe.exe") }, { name: "ffprobe.exe", path: binaryPaths.ffprobe },
{ name: "deno.exe", path: path.join(process.resourcesPath, "binaries", "deno.exe") }, { name: "deno.exe", path: binaryPaths.deno },
]; ];
const missing = deps.filter(dep => !fs.existsSync(dep.path)); const missing = deps.filter(dep => !fs.existsSync(dep.path));
let errorMsg = ""; let errorMsg = "";
@@ -110,36 +114,52 @@ async function createMainWindow() {
} }
function validateDownloadPath(userPath) { function validateDownloadPath(userPath) {
const userHome = os.homedir(); // C:\Users\<User> const { isSafePath } = require("./server/helpers/validation");
if (!userPath) return path.join(userHome, "Downloads", "Freedom Loader"); // Lazy load default path
if (!defaultDownloadPath) {
// Résolution canonique et suivi des symlinks const { defaultDownloadFolder } = require("./server/helpers/path");
const resolved = fs.realpathSync(path.resolve(userPath)); defaultDownloadPath = defaultDownloadFolder;
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; 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 // IPC
ipcMain.handle("select-download-folder", async () => { ipcMain.handle("select-download-folder", async () => {
try { const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] }); if (result.canceled) {
if (!result.canceled && result.filePaths.length > 0) { logger.info("Folder selection cancelled by user");
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}`);
return null; 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) => { 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) => { ipcMain.on("set-progress", (event, percent) => {
if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1 if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1
@@ -157,6 +183,8 @@ ipcMain.on("set-progress", (event, percent) => {
ipcMain.on("window-minimize", () => { ipcMain.on("window-minimize", () => {
if (mainWindow) mainWindow.minimize(); if (mainWindow) mainWindow.minimize();
}); });
// Toggle Maximize -> UnMaximize
ipcMain.on("window-maximize", () => { ipcMain.on("window-maximize", () => {
if (mainWindow) { if (mainWindow) {
if (mainWindow.isMaximized()) { if (mainWindow.isMaximized()) {
@@ -166,6 +194,7 @@ ipcMain.on("window-maximize", () => {
} }
} }
}); });
ipcMain.on("window-close", () => { ipcMain.on("window-close", () => {
if (mainWindow) mainWindow.close(); if (mainWindow) mainWindow.close();
}); });
@@ -248,10 +277,11 @@ app.whenReady().then(async () => {
}); });
configFeatures.discordRPC ? startRPC() : ""; if (configFeatures.discordRPC) startRPC(); // Discord RPC
await createMainWindow(); await createMainWindow();
configFeatures.autoUpdate ? AutoUpdater(mainWindow) : ""; // Auto Update
if (configFeatures.autoUpdate) AutoUpdater(mainWindow); // Auto Update
} catch (err) { } catch (err) {
logger.error("Window or Server error :", err); logger.error("Window or Server error :", err);
@@ -260,11 +290,12 @@ app.whenReady().then(async () => {
}); });
app.on("window-all-closed", () => { app.on("window-all-closed", () => {
logger.info("All Window Closed, shuting down app"); logger.info("Shuting Down App...");
if (process.platform !== "darwin") app.quit(); app.quit();
}); });
app.on("before-quit", () => { app.on("before-quit", async () => {
logSessionEnd() await stopRPC();
stopRPC() logger.info("All Services Stopped. Have a nice day!")
logSessionEnd();
}); });

10
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "freedom-loader", "name": "freedom-loader",
"version": "1.4.3", "version": "1.4.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "freedom-loader", "name": "freedom-loader",
"version": "1.4.3", "version": "1.4.4",
"dependencies": { "dependencies": {
"chalk": "^4.1.2", "chalk": "^4.1.2",
"debug": "^4.4.1", "debug": "^4.4.1",
@@ -3990,9 +3990,9 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "10.2.0", "version": "10.2.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz",
"integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {

View File

@@ -1,7 +1,7 @@
{ {
"name": "freedom-loader", "name": "freedom-loader",
"productName": "Freedom Loader", "productName": "Freedom Loader",
"version": "1.4.3", "version": "1.4.4",
"author": "MasterAcnolo", "author": "MasterAcnolo",
"description": "Freedom Loader", "description": "Freedom Loader",
"main": "main.js", "main": "main.js",

View File

@@ -1,5 +1,7 @@
async function versionLabel(){ async function versionLabel(){
const appVersion = await window.electronAPI.getVersion(); const appVersion = await window.electronAPI.getVersion();
// Write in front the app version for debugging (bottom right)
document.getElementById("version-badge").textContent = `v${appVersion}`; document.getElementById("version-badge").textContent = `v${appVersion}`;
}; };

View File

@@ -45,18 +45,15 @@ window.addEventListener("DOMContentLoaded", async () => {
.getElementById("changePath") .getElementById("changePath")
.addEventListener("click", async () => { .addEventListener("click", async () => {
try { try {
const selectedPath = // selectDownloadFolder already returns a validated path
const validatedPath =
await window.electronAPI.selectDownloadFolder(); await window.electronAPI.selectDownloadFolder();
if (!selectedPath) return; // annulé if (!validatedPath) return; // cancelled
// Validation back obligatoire
const validatedPath =
await window.electronAPI.getValidatedDownloadPath(selectedPath);
await applyPathFromBack(validatedPath); await applyPathFromBack(validatedPath);
} catch (err) { } catch (err) {
alert("Dossier non autorisé."); alert("This folder is not allowed. Only specific folders (Users, Downloads, Documents) are authorized for downloads.");
} }
}); });
}); });

View File

@@ -78,9 +78,27 @@ async function init() {
if (data.type === "playlist") { if (data.type === "playlist") {
infoDiv.classList.add("playlist-mode"); infoDiv.classList.add("playlist-mode");
infoDiv.innerHTML = ` infoDiv.innerHTML = `
<h3 style="color:var(--video-info-heading-color);"><strong>Playlist Detected: ${data.title}</strong></h3> <div class="playlist-header">
<h3 style="color:var(--video-info-heading-color);"><strong>Video Count: ${data.count}</strong></h3> <div class="playlist-info">
<p><strong>Channel :</strong> ${data.channel || "Unknown"}</p> <div class="playlist-badge">Detected Playlist</div>
<h3 class="playlist-title">${data.title}</h3>
<div class="playlist-meta">
<span class="playlist-count">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 6H20M4 12H20M4 18H11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
${data.count} vidéos
</span>
${data.channel ? `<span class="playlist-channel">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="2"/>
<path d="M5 20C5 16.134 8.134 13 12 13C15.866 13 19 16.134 19 20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
${data.channel}
</span>` : ''}
</div>
</div>
</div>
<div id="playlistVideos"></div> <div id="playlistVideos"></div>
`; `;
@@ -88,39 +106,58 @@ async function init() {
data.videos.forEach(v => { data.videos.forEach(v => {
const durationStr = v.duration 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; const videoUrl = v.id ? `https://www.youtube.com/watch?v=${v.id}` : v.url;
listDiv.innerHTML += ` listDiv.innerHTML += `
<div style="margin-bottom:12px;"> <div class="playlist-video-card">
<img src="${v.thumbnail}" width="160" alt="Thumbnail"> <div class="video-thumbnail-wrapper">
<p><strong>${v.title}</strong></p> <img src="${v.thumbnail}" alt="${v.title}" class="video-thumbnail">
<p>Duration : ${durationStr}</p> <span class="video-duration">${durationStr}</span>
<p><a href="${videoUrl}" target="_blank">URL</a> </div>
<button class="copy-btn" data-url="${videoUrl}">📋</button> <div class="video-content">
</p> <h4 class="video-title" title="${v.title}">${v.title}</h4>
${v.uploader ? `<p class="video-uploader">${v.uploader}</p>` : ''}
<div class="video-actions">
<a href="${videoUrl}" target="_blank" class="video-link">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Ouvrir
</a>
<button class="copy-btn" data-url="${videoUrl}" title="Copier l'URL">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="8" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
</div>
</div> </div>
`; `;
}); });
// Gestion du bouton copier // Gestion du bouton copier
listDiv.addEventListener("click", (event) => { listDiv.addEventListener("click", (event) => {
if (event.target.classList.contains("copy-btn")) { if (event.target.classList.contains("copy-btn") || event.target.closest(".copy-btn")) {
const btn = event.target; const btn = event.target.closest(".copy-btn") || event.target;
if (btn.disabled) return; if (btn.disabled) return;
btn.disabled = true; btn.disabled = true;
const url = btn.dataset.url; const url = btn.dataset.url;
navigator.clipboard.writeText(url) navigator.clipboard.writeText(url)
.then(() => { .then(() => {
const original = btn.textContent; const original = btn.innerHTML;
btn.style.opacity = 0; btn.style.opacity = 0;
btn.style.transform = "scale(0.7)"; btn.style.transform = "scale(0.7)";
setTimeout(() => { setTimeout(() => {
btn.textContent = "✅"; btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
btn.style.opacity = 1; btn.style.opacity = 1;
btn.style.transform = "scale(1)"; btn.style.transform = "scale(1)";
@@ -129,7 +166,7 @@ async function init() {
btn.style.transform = "scale(0.7)"; btn.style.transform = "scale(0.7)";
setTimeout(() => { setTimeout(() => {
btn.textContent = original; btn.innerHTML = original;
btn.style.opacity = 1; btn.style.opacity = 1;
btn.style.transform = "scale(1)"; btn.style.transform = "scale(1)";
btn.disabled = false; btn.disabled = false;
@@ -140,10 +177,12 @@ async function init() {
}, 300); }, 300);
}) })
.catch(() => { .catch(() => {
const original = btn.textContent; const original = btn.innerHTML;
btn.textContent = "❌"; btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
</svg>`;
setTimeout(() => { setTimeout(() => {
btn.textContent = original; btn.innerHTML = original;
btn.disabled = false; btn.disabled = false;
}, 1500); }, 1500);
}); });

View File

@@ -69,80 +69,293 @@
/* --- Playlist Mode --- */ /* --- Playlist Mode --- */
#videoInfo.playlist-mode { #videoInfo.playlist-mode {
max-width: 900px; max-width: 95%;
} }
/* Style du bloc "Playlist détectée" */ /* Playlist Header */
#videoInfo.playlist-mode > p { .playlist-header {
text-align: center; display: flex;
font-size: 1.1rem; 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; 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 */ /* Layout liste des vidéos */
#playlistVideos { #playlistVideos {
margin-top: 1em; margin-top: 1em;
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 18px; gap: 1.5rem;
} }
/* Un item de playlist */ /* Carte vidéo playlist */
#playlistVideos > div { .playlist-video-card {
background: var(--playlist-background-color); background: var(--playlist-background-color);
border-radius: 10px; border-radius: 14px;
padding: 10px; overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); box-shadow: 0 2px 12px rgba(0,0,0,0.12);
text-align: center; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
transition: transform 0.2s ease;
}
#playlistVideos > div p {
display: flex; display: flex;
align-items: center; flex-direction: column;
justify-content: center; border: 1px solid rgba(255, 255, 255, 0.1);
flex-wrap: wrap; position: relative;
} }
#videoInfo.playlist-mode { .playlist-video-card::before {
max-width: 95%; 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); transform: scale(1.05);
} }
#playlistVideos img { .video-duration {
width: 100%; 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; 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 { .copy-btn {
all: unset; background: rgba(255, 255, 255, 0.06);
background: transparent; border: 1px solid rgba(255, 255, 255, 0.08);
border: none;
cursor: pointer; cursor: pointer;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 4px; padding: 0.5rem;
vertical-align: middle; border-radius: 8px;
transition: transform 0.1s ease, color 0.1s ease, opacity 0.15s ease; transition: all 0.2s ease;
font-size: 1em; width: 40px;
height: 40px;
flex-shrink: 0;
} }
#playlistVideos > div p a{ .copy-btn svg {
text-decoration: none; width: 18px;
color: var(--video-info-link-color); height: 18px;
color: var(--video-info-text-color);
} }
#playlistVideos > div p a:hover{ .copy-btn:hover {
text-decoration: underline; 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 */ /* 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) { @media (max-width: 480px) {
#videoInfo { #videoInfo {
padding: 1em; padding: 1em;
@@ -152,4 +365,21 @@
#videoInfo h3 { #videoInfo h3 {
font-size: 1.4rem; 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;
}
} }

View File

@@ -16,10 +16,14 @@ async function downloadController(req, res) {
}; };
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("❌ Invalid URL !"); 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); // Get output folder when the download is finished
notifyDownloadFinished(filePath); const outputFolder = await fetchDownload(options, listeners, speedListeners);
notifyDownloadFinished(outputFolder);
res.send("✅ Download Done !"); res.send("✅ Download Done !");
} catch (err) { } catch (err) {

View File

@@ -13,7 +13,7 @@ async function infoController(req, res){
logger.info(`/Info Request receive by the Info Controller. URL: ${url}`); 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") logger.info("Estimated Data Type: Playlist")
} else{ } else{

View File

@@ -2,6 +2,7 @@ const fs = require("fs");
const os = require("os"); const os = require("os");
const path = require("path"); const path = require("path");
const notify = require("./notify") const notify = require("./notify")
const { logger } = require("../logger");
function getUserBrowser() { function getUserBrowser() {
const userProfile = os.homedir(); const userProfile = os.homedir();
@@ -18,15 +19,21 @@ function getUserBrowser() {
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") } // { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
]; ];
// Chercher un navigateur disponible
for (const browser of browsers) { for (const browser of browsers) {
if (fs.existsSync(browser.path)) { if (fs.existsSync(browser.path)) {
logger.info(`Browser found: ${browser.name}`);
return 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 module.exports = getUserBrowser

View File

@@ -1,12 +1,11 @@
const { Notification, shell } = require("electron"); const { Notification, shell } = require("electron");
const path = require("path"); const { iconPaths } = require("./path");
function notifyDownloadFinished(folder) { function notifyDownloadFinished(folder) {
const iconPath = path.join(process.resourcesPath, "confirm-icon.png");
const notif = new Notification({ const notif = new Notification({
title: "Freedom Loader", title: "Freedom Loader",
body: "Your download is complete, click here to open it.", body: "Your download is complete, click here to open it.",
icon: iconPath, icon: iconPaths.confirm,
}); });
notif.on("click", () => shell.openPath(folder)); notif.on("click", () => shell.openPath(folder));
@@ -14,11 +13,10 @@ function notifyDownloadFinished(folder) {
} }
function notifyCookiesBrowserError(){ function notifyCookiesBrowserError(){
const iconPath = path.join(process.resourcesPath, "error.png");
const notif = new Notification({ const notif = new Notification({
title: "Cookies Error", title: "Cookies Error",
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.", 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")); notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"));
@@ -26,11 +24,10 @@ function notifyCookiesBrowserError(){
} }
function notifyFirefoxBrowserMissing(){ function notifyFirefoxBrowserMissing(){
const iconPath = path.join(process.ressourcesPath, "error.png");
const notif = new Notification({ const notif = new Notification({
title: "Firefox Missing", title: "Firefox Missing",
body: "Firefox was not found on your system. Click here to follow the installation guide", 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")) notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"))

View File

@@ -2,12 +2,6 @@ function parseVideo(data) {
return { return {
type: "video", type: "video",
// id: data.id,
// title: data.title,
// url: data.webpage_url,
// duration: data.duration,
// thumbnail: data.thumbnail,
// uploader: data.uploader
...data ...data
}; };
} }

View File

@@ -1,15 +1,25 @@
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const os = require("os");
const { app } = require("electron"); const { app } = require("electron");
const config = require("../../config"); const config = require("../../config");
const { logger } = require("../logger.js"); 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 userYtDlp;
let ffmpegPath; let ffmpegPath;
let denoPath; let denoPath;
const sourceYtDlp = path.join(process.resourcesPath, "binaries","yt-dlp.exe"); const sourceYtDlp = path.join(resourcesPath, "binaries","yt-dlp.exe");
if (config.localMode) { if (config.localMode) {
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe"); userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe");
@@ -17,8 +27,8 @@ if (config.localMode) {
denoPath = path.join(__dirname, "../../ressources/deno.exe"); denoPath = path.join(__dirname, "../../ressources/deno.exe");
} else { } else {
userYtDlp = path.join(app.getPath("userData"),"yt-dlp.exe"); userYtDlp = path.join(app.getPath("userData"),"yt-dlp.exe");
ffmpegPath = path.join(process.resourcesPath, "binaries","ffmpeg.exe"); ffmpegPath = path.join(resourcesPath, "binaries","ffmpeg.exe");
denoPath = path.join(process.resourcesPath, "binaries","deno.exe"); denoPath = path.join(resourcesPath, "binaries","deno.exe");
if (!fs.existsSync(userYtDlp)) { if (!fs.existsSync(userYtDlp)) {
fs.copyFileSync(sourceYtDlp, 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 (!userYtDlp){ logger.error("Missing YT-DLP")}
if (!ffmpegPath){ logger.error("Missing FFMPEG")} if (!ffmpegPath){ logger.error("Missing FFMPEG")}
if (!denoPath){ logger.error("Missing DENO")} if (!denoPath){ logger.error("Missing DENO")}
module.exports = { userYtDlp, ffmpegPath, denoPath }; module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder };

View File

@@ -3,7 +3,7 @@ const RateLimit = require("express-rate-limit");
/* Rate Limite */ /* Rate Limite */
const rateLimite = RateLimit({ 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 max: 5, // limit each IP to 100 requests per windowMs on the root path
message: "Too many requests, please try again later.", message: "Too many requests, please try again later.",
statusCode: 429, // HTTP status code for "Too Many Requests" statusCode: 429, // HTTP status code for "Too Many Requests"

View File

@@ -13,7 +13,7 @@ function isValidUrl(url) {
function isSafePath(folder) { function isSafePath(folder) {
if (!folder || folder.length < 3) return false; if (!folder || folder.length < 3) return false;
const unsafe = ["System32", "/etc", "\\Windows"]; const unsafe = ["System32", "\\Windows"];
const resolved = path.resolve(folder); const resolved = path.resolve(folder);
return !unsafe.some(u => resolved.includes(u)); return !unsafe.some(u => resolved.includes(u));
} }

View File

@@ -3,13 +3,17 @@ const DailyRotateFile = require("winston-daily-rotate-file");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const os = require("os"); const os = require("os");
const config = require("../config") const config = require("../config");
// Dossier de logs Windows // Dossier de logs Windows
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs"); const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
// Création du dossier "logs" si nécessaire // 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( const logFormat = format.combine(
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), 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() { function logSessionStart() {
logger.info(getSessionStartLine()); logger.info(`--- Starting session: ${new Date().toISOString()} ---`);
logger.info(`Application Version: ${config.version}`) logger.info(`Application Version: ${config.version}`)
} }
function logSessionEnd() { function logSessionEnd() {
logger.info(getSessionEndLine()); logger.info(`--- Ending session: ${new Date().toISOString()} ---`);
} }
module.exports = { module.exports = {

View File

@@ -1,5 +1,4 @@
const express = require("express"); const express = require("express");
const fs = require("fs");
const path = require("path"); const path = require("path");
const { logger, logSessionEnd } = require("./logger"); const { logger, logSessionEnd } = require("./logger");
const config = require("../config"); const config = require("../config");
@@ -11,19 +10,7 @@ const app = express();
app.use(express.json()); app.use(express.json());
// Dossier de téléchargement // Mise à jour yt-dlp au démarrage
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
execFile(userYtDlp, ["-U"], (err, stdout, stderr) => { execFile(userYtDlp, ["-U"], (err, stdout, stderr) => {
if (err) logger.warn("yt-dlp update error:", err); if (err) logger.warn("yt-dlp update error:", err);
else logger.info(`Update yt-dlp : ${stdout}`); else logger.info(`Update yt-dlp : ${stdout}`);

View File

@@ -1,18 +1,35 @@
const { execFile } = require("child_process"); const { execFile } = require("child_process");
const { userYtDlp } = require("../helpers/path"); const { userYtDlp, defaultDownloadFolder } = require("../helpers/path");
const path = require("path");
const fs = require("fs"); const fs = require("fs");
const { logger } = require("../logger"); const { logger } = require("../logger");
const { buildYtDlpArgs } = require("../helpers/buildArgs"); 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) { function fetchDownload(options, listeners, speedListeners) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const outputFolder = options.outputFolder || path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader"); const outputFolder = options.outputFolder || defaultDownloadFolder;
fs.mkdirSync(outputFolder, { recursive: true });
// 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(" ")}`); logger.info(`[yt-dlp args] ${args.join(" ")}`);
const child = execFile(userYtDlp, args); const child = execFile(userYtDlp, args);
@@ -21,7 +38,7 @@ function fetchDownload(options, listeners, speedListeners) {
child.on("close", code => { child.on("close", code => {
listeners.forEach(fn => fn("done")); 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}`)); else reject(new Error(`YT-DLP failed with code : ${code}`));
}); });
@@ -31,7 +48,7 @@ function fetchDownload(options, listeners, speedListeners) {
if (!line.trim()) return; if (!line.trim()) return;
logger.info(`[yt-dlp] ${line}`); logger.info(`[yt-dlp] ${line}`);
/* Barre de Chargement*/ /* Progress Bar */
if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset")); if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset"));
const match = line.match(/\[download\]\s+(\d+\.\d+)%/); const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
if (match) listeners.forEach(fn => fn(parseFloat(match[1]))); if (match) listeners.forEach(fn => fn(parseFloat(match[1])));