27 Commits
1.4.2 ... 1.4.4

Author SHA1 Message Date
MasterAcnolo
83ca963502 Merge with false positive
Merge with False positive
2026-02-18 15:10:01 +01:00
MasterAcnolo
9fb9295093 Fix: Try to re run workflow 2026-02-18 14:51:57 +01:00
MasterAcnolo
74fcdbb738 Fix: Undo Path Validation for coming back on a classic one 2026-02-18 14:43:40 +01:00
MasterAcnolo
32c9a66a35 Fix: Centralize default download path and enhance path validation logic 2026-02-17 23:29:31 +01:00
MasterAcnolo
45e0840fea Fix: Correct windowMs value in rate limit configuration to 15 minutes 2026-02-17 23:26:01 +01:00
MasterAcnolo
19b3f8b947 Fix: Refactor conditional statements for starting Discord RPC and Auto Update 2026-02-17 21:49:52 +01:00
MasterAcnolo
cc5bde5b48 Fix: Correct comment for rate limit window duration in rateLimit.js 2026-02-17 21:49:43 +01:00
MasterAcnolo
9a48ac05e3 Fix: Uncomplete comments and remove repeated code 2026-02-17 21:42:12 +01:00
MasterAcnolo
5750444655 Fix: Update full changelog link format in release template 2026-02-17 21:35:57 +01:00
MasterAcnolo
fdb22f2342 Fix: Improve browser detection logging and error handling 2026-02-17 21:29:51 +01:00
MasterAcnolo
760550e521 Fix: Centralize resource paths and icons 2026-02-17 21:28:46 +01:00
MasterAcnolo
334e83ae1f Fix: Small coms in AppVersion.js 2026-02-17 21:12:01 +01:00
MasterAcnolo
5d776594fc Fix: App Not Stopping correctly + Better Logs for end :) 2026-02-17 21:09:09 +01:00
MasterAcnolo
adb36e3fa2 Fix: We are not on apple system. Don't care about Darwin 2026-02-17 20:59:22 +01:00
MasterAcnolo
87945fba80 Fix: Missing semi colon, Better Folder Creation, And remove the fucking bad code who was repeat too much (wtf i did) 2026-02-17 18:05:07 +01:00
MasterAcnolo
2234f9ed81 Fix: Update version numbers to 1.4.4 in package.json and ReadME 2026-02-17 15:22:38 +01:00
MasterAcnolo
7cd3b4be06 Fix: Update playlist badge text to English 2026-02-17 15:19:23 +01:00
MasterAcnolo
a42b020b96 Fix: Update URL check to correctly identify playlist links 2026-02-17 15:19:17 +01:00
MasterAcnolo
c1e70dae80 Feat: Add Better playlist layout 2026-02-17 10:27:13 +01:00
MasterAcnolo
6877c51c0e Fix: Add Small Comms for clarity 2026-02-17 10:26:54 +01:00
MasterAcnolo
5a41090175 Fix: Remove Unusued Code 2026-02-17 10:26:39 +01:00
MasterAcnolo
5983f21759 Fix: Big mistake when estimating video format (? -> &) 2026-02-17 09:50:32 +01:00
MasterAcnolo
ee9e02410f Merge pull request #31 from MasterAcnolo/fix-input
1.4.3
2026-02-16 22:34:12 +01:00
MasterAcnolo
909480083e Fix: Update Dependencies 2026-02-16 22:00:31 +01:00
MasterAcnolo
ca0492ed04 Fix: Update ReadME App Version 2026-02-16 21:37:24 +01:00
MasterAcnolo
6c73dd75be Feat: Add Paste BTN URL Input 2026-02-16 21:34:39 +01:00
MasterAcnolo
556dccec08 Fix: Increment App version 2026-02-16 21:29:24 +01:00
23 changed files with 824 additions and 332 deletions

View File

@@ -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
**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**
[![Release](https://img.shields.io/badge/Release-1.4.2-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/)
<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)
</div>
</div>

99
main.js
View File

@@ -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\<User>
if (!userPath) return path.join(userHome, "Downloads", "Freedom Loader");
// Résolution canonique et suivi des symlinks
const resolved = fs.realpathSync(path.resolve(userPath));
const normalizedHome = path.resolve(userHome) + path.sep;
if (!resolved.startsWith(normalizedHome)) {
throw new Error("Chemin non autorisé : uniquement les sous-dossiers du dossier utilisateur sont permis !");
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();
});

422
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "freedom-loader",
"version": "1.4.1",
"version": "1.4.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "freedom-loader",
"version": "1.4.1",
"version": "1.4.4",
"dependencies": {
"chalk": "^4.1.2",
"debug": "^4.4.1",
@@ -79,6 +79,24 @@
"node": ">=10.12.0"
}
},
"node_modules/@electron/asar/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/@electron/asar/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@electron/asar/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -355,6 +373,13 @@
"node": ">=16.4"
}
},
"node_modules/@electron/universal/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/@electron/universal/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@@ -485,130 +510,14 @@
"node": ">= 10.0.0"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=12"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
"node": ">=18"
}
},
"node_modules/@isaacs/fs-minipass": {
@@ -833,9 +742,9 @@
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
"dev": true,
"license": "MIT"
},
@@ -857,9 +766,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.19.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
"version": "22.19.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
"integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1250,11 +1159,17 @@
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz",
"integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"dependencies": {
"jackspeak": "^4.2.3"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
@@ -1349,14 +1264,16 @@
"optional": true
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
"integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
"balanced-match": "^4.0.2"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/buffer": {
@@ -1510,6 +1427,57 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/cacache/node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/cacache/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/cacache/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/cacache/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/cacache/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@@ -1520,6 +1488,13 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/cacache/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/cacache/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
@@ -1542,6 +1517,22 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/cacache/node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/cacache/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -1565,6 +1556,58 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/cacache/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cacache/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/cacache/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
@@ -2128,6 +2171,24 @@
"p-limit": "^3.1.0 "
}
},
"node_modules/dir-compare/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/dir-compare/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/dir-compare/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -2502,9 +2563,9 @@
}
},
"node_modules/electron-updater/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -2886,6 +2947,13 @@
"minimatch": "^5.0.1"
}
},
"node_modules/filelist/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@@ -3153,6 +3221,24 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3186,9 +3272,9 @@
}
},
"node_modules/global-agent/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"optional": true,
@@ -3569,29 +3655,29 @@
}
},
"node_modules/isexe": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.2.tgz",
"integrity": "sha512-mIcis6w+JiQf3P7t7mg/35GKB4T1FQsBOtMIvuKw4YErj5RjtbhcTd5/I30fmkmGMwvI0WlzSNN+27K0QCMkAw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
"integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=20"
"node": ">=18"
}
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
"integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
"@isaacs/cliui": "^9.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jake": {
@@ -3904,13 +3990,13 @@
}
},
"node_modules/minimatch": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz",
"integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==",
"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": {
"@isaacs/brace-expansion": "^5.0.1"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "20 || >=22"
@@ -4621,9 +4707,9 @@
}
},
"node_modules/qs": {
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -5104,9 +5190,9 @@
}
},
"node_modules/simple-update-notifier/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -5336,9 +5422,9 @@
}
},
"node_modules/tar": {
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {

View File

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

View File

@@ -206,7 +206,14 @@
</header>
<form action="/download" method="POST" id="downloadForm">
<input name="url" placeholder="URL" id="UrlInput">
<div class="url-input-container">
<input type="text" name="url" placeholder="URL" id="UrlInput">
<button type="button" id="pasteBtn" class="paste-btn" title="Paste from clipboard">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 5H7C5.89543 5 5 5.89543 5 7V19C5 20.1046 5.89543 21 7 21H17C18.1046 21 19 20.1046 19 19V7C19 5.89543 18.1046 5 17 5H15M9 5C9 6.10457 9.89543 7 11 7H13C14.1046 7 15 6.10457 15 5M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5M12 12H15M12 16H15M9 12H9.01M9 16H9.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<label class="checkbox-inline">
@@ -263,6 +270,7 @@
<!-- Scripts -->
<script src="script/clipboardPaste.js"></script>
<script src="script/custompath.js"></script>
<script src="script/settingsPanel.js"></script>
<script src="script/appVersion.js"></script>

View File

@@ -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}`;
};

View File

@@ -0,0 +1,35 @@
// Clipboard paste functionality for URL input
document.addEventListener('DOMContentLoaded', () => {
const pasteBtn = document.getElementById('pasteBtn');
const urlInput = document.getElementById('UrlInput');
if (pasteBtn && urlInput) {
pasteBtn.addEventListener('click', async () => {
try {
// Read text from clipboard
const text = await navigator.clipboard.readText();
// Paste into the URL input
urlInput.value = text;
// Trigger input event to ensure any listeners are notified
urlInput.dispatchEvent(new Event('input', { bubbles: true }));
// Optional: Focus the input
urlInput.focus();
// Visual feedback
pasteBtn.style.backgroundColor = 'var(--form-button-bg-hover-color)';
setTimeout(() => {
pasteBtn.style.backgroundColor = '';
}, 200);
} catch (err) {
console.error('Failed to read clipboard:', err);
// Fallback: prompt user if clipboard API fails
alert('Unable to access clipboard. Please paste manually using Ctrl+V.');
}
});
}
});

View File

@@ -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.");
}
});
});

View File

@@ -78,9 +78,27 @@ async function init() {
if (data.type === "playlist") {
infoDiv.classList.add("playlist-mode");
infoDiv.innerHTML = `
<h3 style="color:var(--video-info-heading-color);"><strong>Playlist Detected: ${data.title}</strong></h3>
<h3 style="color:var(--video-info-heading-color);"><strong>Video Count: ${data.count}</strong></h3>
<p><strong>Channel :</strong> ${data.channel || "Unknown"}</p>
<div class="playlist-header">
<div class="playlist-info">
<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>
`;
@@ -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 += `
<div style="margin-bottom:12px;">
<img src="${v.thumbnail}" width="160" alt="Thumbnail">
<p><strong>${v.title}</strong></p>
<p>Duration : ${durationStr}</p>
<p><a href="${videoUrl}" target="_blank">URL</a>
<button class="copy-btn" data-url="${videoUrl}">📋</button>
</p>
<div class="playlist-video-card">
<div class="video-thumbnail-wrapper">
<img src="${v.thumbnail}" alt="${v.title}" class="video-thumbnail">
<span class="video-duration">${durationStr}</span>
</div>
<div class="video-content">
<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>
`;
});
// 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 = `<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.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 = `<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(() => {
btn.textContent = original;
btn.innerHTML = original;
btn.disabled = false;
}, 1500);
});

View File

@@ -62,6 +62,7 @@ form#downloadForm {
transition: border-color 0.3s ease, box-shadow 0.3s ease;
background-color: var(--form-input-bg-color);
color: var(--form-input-text-color);
flex: 1;
}
#UrlInput:focus {
@@ -74,3 +75,39 @@ form#downloadForm {
#UrlInput::placeholder {
color: var(--form-input-placeholder-color);
}
.url-input-container {
display: flex;
align-items: center;
gap: 0.5em;
width: 100%;
}
.paste-btn {
background-color: var(--form-button-bg-color);
color: var(--form-button-text-color);
border: none;
padding: 0.75em;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
height: 44px;
}
.paste-btn:hover {
background-color: var(--form-button-bg-hover-color);
transform: scale(1.05);
}
.paste-btn:active {
transform: scale(0.95);
}
.paste-btn svg {
width: 30px;
height: 30px;
}

View File

@@ -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;
}
}

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.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) {

View File

@@ -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{

View File

@@ -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

View File

@@ -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"))

View File

@@ -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
};
}

View File

@@ -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 };

View File

@@ -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"

View File

@@ -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));
}

View File

@@ -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 = {

View File

@@ -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}`);

View File

@@ -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])));