mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-07-29 18:25:47 +02:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71dbd104f8 | ||
|
|
57a5defb08 | ||
|
|
25c78ff8b7 | ||
|
|
792a59d8d7 | ||
|
|
b13c98dda2 | ||
|
|
1f103c20b9 | ||
|
|
70d62959ca | ||
|
|
83ca963502 | ||
|
|
9fb9295093 | ||
|
|
74fcdbb738 | ||
|
|
32c9a66a35 | ||
|
|
45e0840fea | ||
|
|
19b3f8b947 | ||
|
|
cc5bde5b48 | ||
|
|
9a48ac05e3 | ||
|
|
5750444655 | ||
|
|
fdb22f2342 | ||
|
|
760550e521 | ||
|
|
334e83ae1f | ||
|
|
5d776594fc | ||
|
|
adb36e3fa2 | ||
|
|
87945fba80 | ||
|
|
2234f9ed81 | ||
|
|
7cd3b4be06 | ||
|
|
a42b020b96 | ||
|
|
c1e70dae80 | ||
|
|
6877c51c0e | ||
|
|
5a41090175 | ||
|
|
5983f21759 | ||
|
|
ee9e02410f | ||
|
|
909480083e | ||
|
|
ca0492ed04 | ||
|
|
6c73dd75be | ||
|
|
556dccec08 |
2
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
2
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
@@ -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
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
### **A clean, open-source multimedia downloader for Windows**
|
### **A clean, open-source multimedia downloader for Windows**
|
||||||
|
|
||||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
[](https://masteracnolo.github.io/FreedomLoader/)
|
[](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>
|
||||||
@@ -5,7 +5,8 @@ const path = require("path");
|
|||||||
|
|
||||||
const localMode = !app.isPackaged;
|
const localMode = !app.isPackaged;
|
||||||
|
|
||||||
const featuresPath = localMode ? path.join(__dirname, "./config/config.json") : path.join(path.join(process.resourcesPath,"config/" ,"config.json"));
|
// Change path for JSON file
|
||||||
|
const featuresPath = path.join(path.join(`${localMode ? __dirname : process.resourcesPath}`,"config/" , "config.json"));
|
||||||
|
|
||||||
let features = {};
|
let features = {};
|
||||||
|
|
||||||
|
|||||||
99
main.js
99
main.js
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
2093
package-lock.json
generated
2093
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
"version": "1.4.2",
|
"version": "1.4.5",
|
||||||
"author": "MasterAcnolo",
|
"author": "MasterAcnolo",
|
||||||
"description": "Freedom Loader",
|
"description": "Freedom Loader",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^37.1.0",
|
"electron": "^37.1.0",
|
||||||
"electron-builder": "^26.4.0"
|
"electron-builder": "^25.1.8"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.masteracnolo.freedomloader",
|
"appId": "com.masteracnolo.freedomloader",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
default-src 'self' 'unsafe-inline' data:;
|
default-src 'self' 'unsafe-inline' data:;
|
||||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
||||||
font-src https://fonts.gstatic.com;
|
font-src https://fonts.gstatic.com;
|
||||||
img-src 'self' data: https://i.ytimg.com;
|
img-src 'self' data: https:;
|
||||||
">
|
">
|
||||||
<title></title>
|
<title></title>
|
||||||
|
|
||||||
@@ -206,7 +206,14 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form action="/download" method="POST" id="downloadForm">
|
<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">
|
<label class="checkbox-inline">
|
||||||
|
|
||||||
@@ -263,6 +270,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
|
<script src="script/clipboardPaste.js"></script>
|
||||||
<script src="script/custompath.js"></script>
|
<script src="script/custompath.js"></script>
|
||||||
<script src="script/settingsPanel.js"></script>
|
<script src="script/settingsPanel.js"></script>
|
||||||
<script src="script/appVersion.js"></script>
|
<script src="script/appVersion.js"></script>
|
||||||
|
|||||||
@@ -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}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
35
public/script/clipboardPaste.js
Normal file
35
public/script/clipboardPaste.js
Normal 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.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,12 +43,12 @@ function loadTheme() {
|
|||||||
applyTheme(savedTheme);
|
applyTheme(savedTheme);
|
||||||
themeSelect.value = savedTheme;
|
themeSelect.value = savedTheme;
|
||||||
} else {
|
} else {
|
||||||
applyTheme("dark"); // thème par défaut
|
applyTheme("dark");
|
||||||
themeSelect.value = "dark";
|
themeSelect.value = "dark";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quand l'utilisateur change le thème depuis le select
|
// When the user changes the theme from the select
|
||||||
themeSelect.addEventListener("change", (event) => {
|
themeSelect.addEventListener("change", (event) => {
|
||||||
const selectedTheme = event.target.value;
|
const selectedTheme = event.target.value;
|
||||||
if (themes[selectedTheme]) {
|
if (themes[selectedTheme]) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const button = form.querySelector("button");
|
|||||||
|
|
||||||
form.addEventListener("submit", async (e) => {
|
form.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
button.disabled = true; // Empêche les clics multiples
|
button.disabled = true; // Avoid multiple clicks
|
||||||
statusDiv.textContent = "Download in progress...";
|
statusDiv.textContent = "Download in progress...";
|
||||||
|
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ async function init() {
|
|||||||
|
|
||||||
const url = urlInput.value.trim();
|
const url = urlInput.value.trim();
|
||||||
|
|
||||||
// Si champ vide -> reset total
|
// IF field empty -> total reset
|
||||||
if (!url || url.length < 2) {
|
if (!url || url.length < 2) {
|
||||||
infoDiv.innerHTML = "";
|
infoDiv.innerHTML = "";
|
||||||
infoDiv.classList.remove("visible", "playlist-mode");
|
infoDiv.classList.remove("visible", "playlist-mode");
|
||||||
@@ -58,7 +58,7 @@ async function init() {
|
|||||||
const data = await fetchVideoInfo(url);
|
const data = await fetchVideoInfo(url);
|
||||||
loaderBox.style.display = "none";
|
loaderBox.style.display = "none";
|
||||||
|
|
||||||
// Gestion des erreurs
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
infoDiv.innerHTML = `
|
infoDiv.innerHTML = `
|
||||||
<div style="
|
<div style="
|
||||||
@@ -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
|
// Copy Button
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function resetProgress() {
|
|||||||
speedElement.style.display = "none";
|
speedElement.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connexion SSE
|
// SSE Connexion
|
||||||
const evtSource = new EventSource("/download/progress");
|
const evtSource = new EventSource("/download/progress");
|
||||||
evtSource.onmessage = e => {
|
evtSource.onmessage = e => {
|
||||||
if (e.data === "reset") {
|
if (e.data === "reset") {
|
||||||
@@ -49,10 +49,10 @@ evtSource.onmessage = e => {
|
|||||||
|
|
||||||
if (!isNaN(percent)) {
|
if (!isNaN(percent)) {
|
||||||
updateProgress(percent);
|
updateProgress(percent);
|
||||||
window.electronAPI.setProgress(percent); // update barre des tâches
|
window.electronAPI.setProgress(percent); // Update Task Bar
|
||||||
if (percent >= 100) setTimeout(() => {
|
if (percent >= 100) setTimeout(() => {
|
||||||
resetProgress();
|
resetProgress();
|
||||||
window.electronAPI.setProgress(-1); // retire la barre
|
window.electronAPI.setProgress(-1); // Remove the bar
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function closePanel() {
|
|||||||
settingsPanel.style.display = "none";
|
settingsPanel.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
// charge les features
|
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
const features = await window.electronAPI.getFeatures();
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ async function loadSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ouvrir le JSON
|
// Open The JSON
|
||||||
document.getElementById("open-json-btn").addEventListener("click", () => {
|
document.getElementById("open-json-btn").addEventListener("click", () => {
|
||||||
window.topbarAPI.openConfig(); // ton IPC existant
|
window.topbarAPI.openConfig(); // ton IPC existant
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ form#downloadForm {
|
|||||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
background-color: var(--form-input-bg-color);
|
background-color: var(--form-input-bg-color);
|
||||||
color: var(--form-input-text-color);
|
color: var(--form-input-text-color);
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#UrlInput:focus {
|
#UrlInput:focus {
|
||||||
@@ -74,3 +75,39 @@ form#downloadForm {
|
|||||||
#UrlInput::placeholder {
|
#UrlInput::placeholder {
|
||||||
color: var(--form-input-placeholder-color);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -5,15 +5,16 @@ const { isValidUrl } = require("../helpers/validation");
|
|||||||
|
|
||||||
async function infoController(req, res){
|
async function infoController(req, res){
|
||||||
|
|
||||||
const url = req.body.url || req.query.url; // Gérer POST et GET
|
const url = req.body.url || req.query.url; // POST et GET
|
||||||
|
|
||||||
|
|
||||||
/* Si pas d'url, url non-string ou url invalide*/
|
// If no url, non-string url or invalid url
|
||||||
|
|
||||||
if (!url || typeof url !== "string" || !isValidUrl(url)) return res.status(400).send("❌ Invalid URL Or Missing");
|
if (!url || typeof url !== "string" || !isValidUrl(url)) return res.status(400).send("❌ Invalid URL Or Missing");
|
||||||
|
|
||||||
logger.info(`/Info Request receive by the Info Controller. URL: ${url}`);
|
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{
|
||||||
|
|||||||
@@ -20,10 +20,6 @@ function startRPC() {
|
|||||||
rpc.clearActivity()
|
rpc.clearActivity()
|
||||||
rpc.setActivity(presence);
|
rpc.setActivity(presence);
|
||||||
|
|
||||||
// Met à jour la présence toutes les 15s
|
|
||||||
intervalId = setInterval(() => {
|
|
||||||
rpc.setActivity(presence);
|
|
||||||
}, 15000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
rpc.login({ clientId }).catch(err => {
|
rpc.login({ clientId }).catch(err => {
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ 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();
|
||||||
|
|
||||||
// Liste des navigateurs supportés par yt-dlp (Actuellement je peux que garantir Firefox, Désolé)
|
// List of browsers supported by yt-dlp (Currently I can only guarantee Firefox, Sorry)
|
||||||
const browsers = [
|
const browsers = [
|
||||||
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
|
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
|
||||||
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
|
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
|
||||||
@@ -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") }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Search for an available browser
|
||||||
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 ;
|
// No browser found - notify user
|
||||||
|
logger.warn("No supported browser found on the system");
|
||||||
|
notify.notifyFirefoxBrowserMissing();
|
||||||
|
|
||||||
|
// Fallback: return "Firefox" to let YT-DLP manage error
|
||||||
|
// this avoid app crash
|
||||||
|
return "firefox";
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = getUserBrowser
|
module.exports = getUserBrowser
|
||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,57 @@
|
|||||||
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");
|
||||||
ffmpegPath = path.join(__dirname, "../../ressources/"); // <- contient ffmpeg.exe et ffprobe.exe
|
ffmpegPath = path.join(__dirname, "../../ressources/"); // <- has ffmpeg.exe and ffprobe.exe
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 };
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,24 @@ 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
|
// Logs folder in 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
|
// Create "Logs" folder if needed
|
||||||
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" }),
|
||||||
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Configuration du logger
|
// Logger configuration
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
level: "info",
|
level: "info",
|
||||||
format: logFormat,
|
format: logFormat,
|
||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const { downloadController, progressController, speedController } = require("../
|
|||||||
|
|
||||||
router.post("/", downloadController);
|
router.post("/", downloadController);
|
||||||
|
|
||||||
// SSE pour la progression du téléchargement
|
// SSE for download progress
|
||||||
router.get("/progress", progressController);
|
router.get("/progress", progressController);
|
||||||
router.get("/speed", speedController);
|
router.get("/speed", speedController);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// Update YT-DLP on Startup
|
||||||
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}`);
|
||||||
@@ -37,11 +24,11 @@ app.use(express.static(path.join(__dirname, "../public")));
|
|||||||
app.use("/download", require("./routes/download.route"));
|
app.use("/download", require("./routes/download.route"));
|
||||||
app.use("/info", require("./routes/info.route"));
|
app.use("/info", require("./routes/info.route"));
|
||||||
|
|
||||||
|
// When we get API Root, it return the front pages.
|
||||||
app.get("/", rateLimite ,(req, res) => {
|
app.get("/", rateLimite ,(req, res) => {
|
||||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fonction pour démarrer le serveur
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const server = app.listen(config.applicationPort, () => {
|
const server = app.listen(config.applicationPort, () => {
|
||||||
|
|||||||
@@ -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])));
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ function fetchInfo(url) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
execFile(userYtDlp,[...args, url],
|
execFile(userYtDlp,[...args, url],
|
||||||
{ timeout: 30_000, // 30s, si jamais plus, abandon de la requête
|
{ timeout: 30_000, // 30s, if more timeout
|
||||||
maxBuffer: 10 * 1024 * 1024 }, // 10MO de réponse max (Par défaut: 200ko)
|
maxBuffer: 10 * 1024 * 1024 }, // 10MO max response (Default: 200ko)
|
||||||
|
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user