Compare commits

...

20 Commits

Author SHA1 Message Date
MasterAcnolo
cd43ae10fc fix: bump app version to 1.6.3 since it's no longer a preview 2026-09-10 21:36:33 +02:00
Axel Nicolas
6978277b4b Update codeql.yml 2026-09-10 15:13:02 +02:00
MasterAcnolo
b91c20d450 fix: release.sh release message ending 2026-09-10 15:09:54 +02:00
Axel Nicolas
014b1ad52a Update codeql.yml 2026-09-10 15:09:19 +02:00
Axel Nicolas
ec93d20a39 Create codeql.yml 2026-09-10 15:06:52 +02:00
MasterAcnolo
09c511fcb8 fix: docs in path.helpers 2026-09-10 15:01:50 +02:00
MasterAcnolo
f343fbd148 fix(discord-rpc): remove unusued code 2026-09-10 15:00:00 +02:00
Axel Nicolas
6d99d5f91c Update README.md 2026-09-10 13:55:09 +02:00
MasterAcnolo
0f0bb874bf feat: add logs when minimize and maximize (tray and not) 2026-09-10 12:01:07 +02:00
MasterAcnolo
b9267ec408 fix: enhance package.json with metadata used by electron-builder on Linux 2026-09-10 12:00:20 +02:00
MasterAcnolo
311bff6df5 refactor: systemTray window logic. Window reference is now dynamic and guarded against destroyed states. Minimize can now put window in the systemTray if option is active 2026-09-10 10:27:11 +02:00
MasterAcnolo
e4243a01ae fix: devMode logic 2026-09-10 10:25:03 +02:00
MasterAcnolo
52be9c6b4f update: electron from 43.3.0 to 44.3.0 2026-09-10 10:23:29 +02:00
MasterAcnolo
e541eb442a fix:
- replace unusued throw new Error by logger.error
- remove unusued variables isWindows, err
- remove unusued logFormat
- remove unusued imports logSessionEnd
2026-09-09 09:21:11 +02:00
MasterAcnolo
e2c2dde4a2 fix: README.md 2026-09-09 08:59:55 +02:00
MasterAcnolo
ed4022dfd1 fix: website link in README 2026-09-04 20:36:03 +02:00
MasterAcnolo
fcbc3f95ac fix: remove old and unusued icons 2026-09-03 20:56:04 +02:00
MasterAcnolo
f3293ef89a fix: stopServer message logic 2026-09-01 16:17:58 +02:00
MasterAcnolo
05e864b93a refactor: getBrowser function, and better firefox management on linux 2026-08-22 22:29:38 +02:00
MasterAcnolo
8ec901dc07 feat: centralize isPlaylistUrl function as a helper that could be upgraded in the future 2026-08-22 22:09:01 +02:00
24 changed files with 177 additions and 169 deletions

46
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: "CodeQL PR"
on:
pull_request:
branches: [ "main" ]
schedule:
- cron: '00 00 * * 1'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
security-events: write
packages: read
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'Manual build required'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"

View File

@@ -15,8 +15,6 @@
</div> </div>
---
## Overview ## Overview
Freedom Loader is a desktop application built with Electron that provides a straightforward way to download video and audio content with full metadata support. The application focuses on reliability, transparency, and user privacy-no ads, no tracking, no questionable third-party services. Freedom Loader is a desktop application built with Electron that provides a straightforward way to download video and audio content with full metadata support. The application focuses on reliability, transparency, and user privacy-no ads, no tracking, no questionable third-party services.
@@ -25,7 +23,7 @@ The primary goal is to make media downloading accessible to users who want offli
### See related repo: ### See related repo:
- **[Website](https://masteracnolo.github.io/Freedom-Loader-Site/)** - Official project website and documentation - **[Website](https://github.com/MasterAcnolo/Freedom-Loader-Site/)** - Official project website and documentation
- **[Workshop](https://github.com/MasterAcnolo/Freedom-Loader-Workshop)** - Theme creation tool and theme browser - **[Workshop](https://github.com/MasterAcnolo/Freedom-Loader-Workshop)** - Theme creation tool and theme browser
## Table of Contents ## Table of Contents
@@ -412,7 +410,6 @@ You are free to use, modify, and redistribute this software under the terms of t
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one - [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
- [@AureCX](https://github.com/AureCX) for being the Official Windows Tester! - [@AureCX](https://github.com/AureCX) for being the Official Windows Tester!
- Zakaria for the website icon - Zakaria for the website icon
- Jacques Chirac to love Apples
- All users who test, report issues, and help improve the application - All users who test, report issues, and help improve the application
--- ---

View File

@@ -102,7 +102,7 @@ function startRPC() {
}); });
rpc.login({ clientId }).catch(err => { rpc.login({ clientId }).catch(() => {
// Since 1.6.2, it no longer print the error. This is because if discord is not opened, it will send an error. // Since 1.6.2, it no longer print the error. This is because if discord is not opened, it will send an error.
// logger.error("Unable to connect to the RPC:", err); // logger.error("Unable to connect to the RPC:", err);
logger.info("Unable to connect to the Discord RPC. Discord is maybe not launched"); logger.info("Unable to connect to the Discord RPC. Discord is maybe not launched");
@@ -123,14 +123,6 @@ async function stopRPC() {
try { try {
if (intervalId) clearInterval(intervalId); if (intervalId) clearInterval(intervalId);
/**
* Ensures RPC connection AND the underlying socket exist
* before attempting to clear activity to prevent crash.
*/
if (rpc.transport && rpc.transport.socket) {
await rpc.clearActivity();
}
await rpc.destroy(); await rpc.destroy();
} catch (err) { } catch (err) {

View File

@@ -13,7 +13,7 @@
const { ipcMain, dialog, shell } = require("electron"); const { ipcMain, dialog, shell } = require("electron");
const fs = require("fs"); const fs = require("fs");
const { logger, logDir } = require("../server/logger"); const { logger, logDir } = require("../server/logger");
const { configFeatures, featuresPath } = require("../config"); const {configFeatures, featuresPath, devMode} = require("../config");
const { getThemes, reloadThemes } = require("./themeManager"); const { getThemes, reloadThemes } = require("./themeManager");
const config = require("../config"); const config = require("../config");
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator"); const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
@@ -114,7 +114,18 @@ function registerIpcHandlers(getMainWindow) {
/** /**
* Window minimize request from renderer. * Window minimize request from renderer.
*/ */
ipcMain.on("window-minimize", () => getMainWindow()?.minimize()); ipcMain.on("window-minimize", () => {
// Minimize to tray
if (configFeatures.systemTray) {
getMainWindow()?.hide()
logger.info("Window Minimized in SystemTray (Using Minimize Button)");
} else {
// Native minimize
getMainWindow()?.minimize()
logger.info("Window minimized normally");
}
}
);
/** /**
* Toggles maximize/unmaximize state of main window. * Toggles maximize/unmaximize state of main window.
@@ -223,7 +234,7 @@ function registerIpcHandlers(getMainWindow) {
if (key === "systemTray") { if (key === "systemTray") {
if (value === true) { if (value === true) {
logger.info("System Tray enabled dynamically."); logger.info("System Tray enabled dynamically.");
createSystemTray(getMainWindow(), config.devMode); createSystemTray(devMode);
} else { } else {
logger.info("System Tray disabled dynamically."); logger.info("System Tray disabled dynamically.");
destroyTray(); destroyTray();

View File

@@ -66,7 +66,7 @@ function validateDownloadPath(userPath) {
const real = fs.realpathSync(absolutePath); const real = fs.realpathSync(absolutePath);
if (!isSafePath(real)) { if (!isSafePath(real)) {
throw new Error("Path not allowed: system folders are blocked!"); logger.error("Path not allowed: system folders are blocked!");
} }
return real; return real;

View File

@@ -2,6 +2,7 @@ const {app, Menu, Tray, nativeImage} = require("electron");
const path = require("path"); const path = require("path");
const {logger} = require("../server/logger"); // Ajuste le chemin si besoin const {logger} = require("../server/logger"); // Ajuste le chemin si besoin
const fs = require("fs"); const fs = require("fs");
const {getMainWindow} = require("./windowManager");
/** /**
* Global reference to the Tray instance to prevent garbage collection. * Global reference to the Tray instance to prevent garbage collection.
@@ -17,11 +18,10 @@ let tray = null;
* - Builds the right-click context menu. * - Builds the right-click context menu.
* - Handles left-click behavior to toggle main window visibility. * - Handles left-click behavior to toggle main window visibility.
* *
* @param {import('electron').BrowserWindow} mainWindow - The main application window.
* @param {boolean} devMode - Indicates whether the application is running in development mode. * @param {boolean} devMode - Indicates whether the application is running in development mode.
* @returns {Tray} The created Tray instance. * @returns {Tray} The created Tray instance.
*/ */
function createSystemTray(mainWindow, devMode) { function createSystemTray(devMode) {
// Prevent creating multiple tray instances // Prevent creating multiple tray instances
if (tray) return tray; if (tray) return tray;
@@ -58,7 +58,12 @@ function createSystemTray(mainWindow, devMode) {
{ {
label: "Show Freedom Loader", label: "Show Freedom Loader",
click: () => { click: () => {
mainWindow.show(); const window = getMainWindow();
if (window && !window.isDestroyed()) {
window.show();
window.focus();
logger.info("Window Restored from SystemTray (Using Show Action)");
}
}, },
}, },
{ type: "separator" }, { type: "separator" },
@@ -82,11 +87,15 @@ function createSystemTray(mainWindow, devMode) {
* but it remains standard practice for Windows and Linux environments. * but it remains standard practice for Windows and Linux environments.
*/ */
tray.on("click", () => { tray.on("click", () => {
if (mainWindow.isVisible()) { const window = getMainWindow();
mainWindow.hide(); if (!window || window.isDestroyed()) return;
if (window.isVisible()) {
window.hide();
} else { } else {
mainWindow.show(); window.show();
mainWindow.focus(); window.focus();
logger.info("Window Restored from SystemTray (Double Click on Tray Icon)")
} }
}); });

View File

@@ -87,9 +87,11 @@ async function createMainWindow() {
* cancels the destruction and hides the window instead. * cancels the destruction and hides the window instead.
*/ */
mainWindow.on('close', (event) => { mainWindow.on('close', (event) => {
if (!app.isQuitting && config.systemTray ) {
if (!app.isQuitting && config.configFeatures.systemTray) {
event.preventDefault(); event.preventDefault();
mainWindow.hide(); mainWindow.hide();
logger.info("Window Minimized in SystemTray (Using Close Button)");
return false; return false;
} }
}); });

View File

@@ -7,7 +7,7 @@ const path = require("path");
* Indicates whether the application is running in development mode. * Indicates whether the application is running in development mode.
* Determined by Electron's packaging state. * Determined by Electron's packaging state.
*/ */
const devMode = process.env.NODE_ENV === "test" || !app?.isPackaged; const devMode = !app.isPackaged || process.env.NODE_ENV === "test";
/** /**
* Resolves the configuration file path depending on runtime environment. * Resolves the configuration file path depending on runtime environment.

10
main.js
View File

@@ -38,7 +38,7 @@ app.setAppUserModelId("com.masteracnolo.freedomloader");
* Load the app dependencies for hardware choice * Load the app dependencies for hardware choice
*/ */
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger"); const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
const { configFeatures } = require("./config"); const { configFeatures, devMode} = require("./config");
/** /**
* In-memory snapshot of application feature flags. * In-memory snapshot of application feature flags.
@@ -53,7 +53,7 @@ if (!configFeatures.enableHardwareAcceleration){
logger.info("Enable Hardware Acceleration") logger.info("Enable Hardware Acceleration")
} }
if(configFeatures.devMode){ if(devMode){
/** /**
* Start devTron extensions - @see https://github.com/electron/devtron * Start devTron extensions - @see https://github.com/electron/devtron
*/ */
@@ -74,7 +74,7 @@ const { updateYtDlp } = require("./app/ytDlpUpdater");
const { createMainWindow, getMainWindow } = require("./app/windowManager"); const { createMainWindow, getMainWindow } = require("./app/windowManager");
const { registerIpcHandlers } = require("./app/ipcHandlers"); const { registerIpcHandlers } = require("./app/ipcHandlers");
const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager"); const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager");
const { userThemesPath, initUserThemes, isWindows, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers"); const { userThemesPath, initUserThemes, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
const {createSystemTray, destroyTray} = require("./app/tray"); const {createSystemTray, destroyTray} = require("./app/tray");
const { stopServer } = require("./server/server"); const { stopServer } = require("./server/server");
@@ -135,7 +135,7 @@ app.whenReady().then(async () => {
createSplashWindow(); createSplashWindow();
if (!configFeatures.devMode && !checkNativeDependencies()) return; if (!devMode && !checkNativeDependencies()) return;
const { userYtDlp } = require("./server/helpers/path.helpers"); const { userYtDlp } = require("./server/helpers/path.helpers");
updateYtDlp(userYtDlp); updateYtDlp(userYtDlp);
@@ -161,7 +161,7 @@ app.whenReady().then(async () => {
getMainWindow().show(); getMainWindow().show();
if (configFeatures.systemTray) { if (configFeatures.systemTray) {
createSystemTray(getMainWindow(), configFeatures.devMode); createSystemTray(devMode);
} }
if (configFeatures.discordRPC) startRPC(); if (configFeatures.discordRPC) startRPC();

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "freedom-loader", "name": "freedom-loader",
"version": "1.6.2-preview", "version": "1.6.3-preview",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "freedom-loader", "name": "freedom-loader",
"version": "1.6.2-preview", "version": "1.6.3-preview",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"dependencies": { "dependencies": {
"chalk": "^4.1.2", "chalk": "^4.1.2",
@@ -24,7 +24,7 @@
"@electron/devtron": "^2.1.1", "@electron/devtron": "^2.1.1",
"@jest/globals": "^30.4.1", "@jest/globals": "^30.4.1",
"@playwright/test": "^1.62.1", "@playwright/test": "^1.62.1",
"electron": "^43.3.0", "electron": "^44.3.0",
"electron-builder": "^26.15.3", "electron-builder": "^26.15.3",
"jest": "^30.4.2", "jest": "^30.4.2",
"nodemon": "^3.1.14", "nodemon": "^3.1.14",
@@ -3910,9 +3910,9 @@
} }
}, },
"node_modules/electron": { "node_modules/electron": {
"version": "43.3.0", "version": "44.3.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-43.3.0.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-44.3.0.tgz",
"integrity": "sha512-nLlvu0WFjftWsSaTkV2B/c4NDuJBspTyXu8vKSQ6vLvFt8uG3NgN49LLKcXddwX0GqVvAQDhciWp+4xOdTdhew==", "integrity": "sha512-St9EV7F2VtYaYWD2qaAjBwUgKxx39eJOUsUJ5+/1113sqbVfNqv4Dbm/W1rN7qmYSPa+mWwR6yr+b7MfgjgVfQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -1,7 +1,8 @@
{ {
"name": "freedom-loader", "name": "freedom-loader",
"desktopName": "com.masteracnolo.freedomloader",
"productName": "Freedom Loader", "productName": "Freedom Loader",
"version": "1.6.3-preview", "version": "1.6.3",
"author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>", "author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
"description": "Free and open-source GUI for yt-dlp — download video and audio from hundreds of platforms", "description": "Free and open-source GUI for yt-dlp — download video and audio from hundreds of platforms",
"homepage": "https://masteracnolo.github.io/Freedom-Loader-Site/", "homepage": "https://masteracnolo.github.io/Freedom-Loader-Site/",
@@ -21,7 +22,6 @@
"dev": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"electron --no-warnings .\"", "dev": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"electron --no-warnings .\"",
"dev:warn": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"electron --trace-warnings .\"", "dev:warn": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"electron --trace-warnings .\"",
"dev:debug": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"WAYLAND_DEBUG=1 ELECTRON_ENABLE_LOGGING=1 ELECTRON_ENABLE_STACK_DUMPING=1 electron --enable-logging --v=1 .\"", "dev:debug": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"WAYLAND_DEBUG=1 ELECTRON_ENABLE_LOGGING=1 ELECTRON_ENABLE_STACK_DUMPING=1 electron --enable-logging --v=1 .\"",
"build": "electron-builder", "build": "electron-builder",
"build:win": "electron-builder --win", "build:win": "electron-builder --win",
"build:linux": "electron-builder --linux", "build:linux": "electron-builder --linux",
@@ -30,10 +30,8 @@
"release": "bash scripts/release.sh", "release": "bash scripts/release.sh",
"release:publish": "bash scripts/release.sh --publish", "release:publish": "bash scripts/release.sh --publish",
"release:dry-run": "bash scripts/release.sh --dry-run", "release:dry-run": "bash scripts/release.sh --dry-run",
"test": "jest", "test": "jest",
"test:unit": "jest test/unit", "test:unit": "jest test/unit",
"update": "npm update" "update": "npm update"
}, },
"dependencies": { "dependencies": {
@@ -52,7 +50,7 @@
"@electron/devtron": "^2.1.1", "@electron/devtron": "^2.1.1",
"@jest/globals": "^30.4.1", "@jest/globals": "^30.4.1",
"@playwright/test": "^1.62.1", "@playwright/test": "^1.62.1",
"electron": "^43.3.0", "electron": "^44.3.0",
"electron-builder": "^26.15.3", "electron-builder": "^26.15.3",
"jest": "^30.4.2", "jest": "^30.4.2",
"nodemon": "^3.1.14", "nodemon": "^3.1.14",
@@ -136,6 +134,8 @@
"snap" "snap"
], ],
"category": "Utility", "category": "Utility",
"synopsis": "GUI for yt-dlp to download video and audio with metadata",
"maintainer": "MasterAcnolo",
"icon": "build/app-icon.png", "icon": "build/app-icon.png",
"executableName": "freedom-loader", "executableName": "freedom-loader",
"extraResources": [ "extraResources": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -108,16 +108,6 @@ RELEASE_NOTES="# Freedom Loader - $VERSION
## Found a bug or issue? ## Found a bug or issue?
Please report it in the [GitHub Issues](https://github.com/MasterAcnolo/Freedom-Loader/issues) section. Please report it in the [GitHub Issues](https://github.com/MasterAcnolo/Freedom-Loader/issues) section.
## Next Release (non-exhaustive roadmap)
- More format options
- Subtitle support
- Improved UI / UX
- Language selection
- Download specific parts of videos
- File renaming options
- Parallel downloads
- Skip sponsored parts automatically
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/${PREV_TAG}...${TAG}" **Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/${PREV_TAG}...${TAG}"
if [ "$DRY_RUN" = false ]; then if [ "$DRY_RUN" = false ]; then

View File

@@ -2,6 +2,7 @@ const { fetchInfo } = require("../services/info.services");
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers"); const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers");
const { logger } = require("../logger"); const { logger } = require("../logger");
const { isValidUrl } = require("../helpers/validation.helpers"); const { isValidUrl } = require("../helpers/validation.helpers");
const {isUrlPlaylist} = require("../helpers/playlist.helpers");
/** /**
* Handles metadata retrieval for a video or playlist. * Handles metadata retrieval for a video or playlist.
@@ -36,7 +37,7 @@ async function infoController(req, res) {
logger.info(`Info request received. ENCODED: ${encodedUrl}`); logger.info(`Info request received. ENCODED: ${encodedUrl}`);
// Lightweight heuristic to detect playlist URLs. It works for Youtube. // Lightweight heuristic to detect playlist URLs. It works for Youtube.
const isPlaylistUrl = url.includes("&list") || url.includes("?list"); const isPlaylistUrl = isUrlPlaylist(url);
logger.info( logger.info(
isPlaylistUrl isPlaylistUrl
@@ -76,11 +77,11 @@ async function infoController(req, res) {
); );
// Preserve original error context // Preserve original error context
throw err; logger.error(err);
} }
} else { } else {
throw err; logger.error(err);
} }
} }

View File

@@ -1,6 +1,6 @@
const path = require("path"); const path = require("path");
const getUserBrowser = require("./getBrowser.helpers.js"); const {getUserBrowser} = require("./getBrowser.helpers.js");
const { ffmpegPath, denoPath} = require("./path.helpers.js"); const { ffmpegPath, denoPath} = require("./path.helpers.js");
const { configFeatures } = require("../../config.js"); const { configFeatures } = require("../../config.js");
const { logger } = require("../logger.js"); const { logger } = require("../logger.js");

View File

@@ -1,7 +1,6 @@
const fs = require("fs"); const fs = require("fs");
const os = require("os"); const os = require("os");
const path = require("path"); const notify = require("./notify.helpers");
const notify = require("./notify.helpers")
const { logger } = require("../logger"); const { logger } = require("../logger");
/** /**
@@ -22,41 +21,38 @@ const { logger } = require("../logger");
* @returns {string} The detected browser identifier. * @returns {string} The detected browser identifier.
*/ */
function getUserBrowser() { function getUserBrowser() {
//const userProfile = os.homedir(); const platform = process.platform;
const firefoxPath = getFirefoxProfilePath(); let isFirefoxHere = false;
/** if (platform === "win32") {
* Browsers supported by yt-dlp cookie extraction. const paths = [
* "C:\\Program Files\\Mozilla Firefox\\firefox.exe",
* Only Firefox is currently enabled and tested. "C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe",
* Additional browsers can be enabled in the future `${process.env.LOCALAPPDATA}\\Mozilla Firefox\\firefox.exe`,
* once compatibility has been verified. ];
*/
const browsers = [
{ name: "firefox", path: firefoxPath },
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
// { name: "brave", path: path.join(userProfile, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default") },
// { name: "edge", path: path.join(userProfile, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default") },
// { name: "opera", path: path.join(userProfile, "AppData", "Roaming", "Opera Software", "Opera Stable") },
// { name: "vivaldi", path: path.join(userProfile, "AppData", "Local", "Vivaldi", "User Data", "Default") },
// { name: "safari", path: path.join(userProfile, "AppData", "Local", "Apple Computer", "Safari") },
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
];
// Search for the first available browser profile. isFirefoxHere = paths.some(p => fs.existsSync(p));
for (const browser of browsers) {
if (browser.path && fs.existsSync(browser.path)) {
logger.info(`Browser found: ${browser.name}`);
return browser.name;
}
} }
// No supported browser found => Notify User if (platform === "linux") {
logger.warn("No supported browser found on the system"); const paths = [
"/usr/bin/firefox",
"/usr/bin/firefox-esr",
"/usr/local/bin/firefox",
"/snap/bin/firefox",
"/var/lib/flatpak/exports/bin/org.mozilla.firefox",
`${os.homedir()}/.local/share/flatpak/exports/bin/org.mozilla.firefox`,
];
isFirefoxHere = paths.some(p => fs.existsSync(p));
}
// If you somehow managed to live without Firefox and need help installing it. - Don't applied to my Linux chad if (!isFirefoxHere) {
if (process.platform === "win32") { // If you somehow managed to live without Firefox and need help installing it. - Don't applied to my Linux chad
// No supported browser found => Notify User
logger.warn("No supported browser found on the system");
notify.notifyFirefoxBrowserMissing(); notify.notifyFirefoxBrowserMissing();
} else {
logger.info("Browser found: firefox");
} }
// Fallback to Firefox and let yt-dlp handle the error gracefully. // Fallback to Firefox and let yt-dlp handle the error gracefully.
@@ -64,39 +60,4 @@ function getUserBrowser() {
return "firefox"; return "firefox";
} }
function getFirefoxProfilePath() { module.exports = {getUserBrowser};
const home = os.homedir();
if (process.platform === "win32") {
return path.join(
home,
"AppData",
"Roaming",
"Mozilla",
"Firefox",
"Profiles"
);
}
if (process.platform === "linux") {
return path.join(
"usr",
"bin",
"firefox"
);
}
if (process.platform === "darwin") {
return path.join(
home,
"Library",
"Application Support",
"Firefox",
"Profiles"
);
}
return null;
}
module.exports = getUserBrowser

View File

@@ -260,7 +260,7 @@ function validateBinaries() {
/** /**
* Helper to lazy load logger, avoid circular import * Helper to lazy load logger, avoid circular import
* @returns {winston.Logger} * @returns {import("winston").Logger}
*/ */
function getLogger() { function getLogger() {
return require("../logger.js").logger; return require("../logger.js").logger;

View File

@@ -0,0 +1,12 @@
/**
* Determines whether a URL targets a playlist.
*
* The detection is based on the presence of the YouTube
* playlist query parameter (`list`).
*
* @param {string} url - URL to inspect.
* @returns {boolean} True if the URL appears to be a playlist.
*/
export function isUrlPlaylist(url) {
return url.includes("?list=") || url.includes("&list=") || url.includes("@");
}

View File

@@ -17,11 +17,6 @@ try {
console.error(`Failed to create log directory: ${error.message}`); console.error(`Failed to create log directory: ${error.message}`);
} }
const logFormat = format.combine(
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
);
/** /**
* Format used by the saved file * Format used by the saved file
* @type {Format} * @type {Format}

View File

@@ -2,7 +2,7 @@ const express = require("express");
const path = require("path"); const path = require("path");
const config = require("../config"); const config = require("../config");
const { logger, logSessionEnd } = require("./logger"); const {logger} = require("./logger");
const { rateLimit } = require("./helpers/rateLimit.helpers"); const { rateLimit } = require("./helpers/rateLimit.helpers");
const app = express(); const app = express();
@@ -53,11 +53,14 @@ async function startServer() {
*/ */
function stopServer() { function stopServer() {
if (serverInstance) { if (serverInstance) {
serverInstance.close(); try{
logger.info("Express server closed cleanly."); serverInstance.close();
logger.info("Express server closed cleanly");
} catch (err){
logger.error("Express server was not closed cleanly:", err)
}
} else { } else {
logger.error("Express server was not closed cleanly") logger.info("Express server was already closed");
} }
} }

View File

@@ -7,6 +7,7 @@ const notify = require("../helpers/notify.helpers");
const path = require("path"); const path = require("path");
const { isSafePath } = require("../helpers/validation.helpers"); const { isSafePath } = require("../helpers/validation.helpers");
const {reloadFeatures} = require("../../config"); const {reloadFeatures} = require("../../config");
const {isUrlPlaylist} = require("../helpers/playlist.helpers");
/** /**
* Reference to the currently running yt-dlp process. * Reference to the currently running yt-dlp process.
@@ -18,19 +19,6 @@ const {reloadFeatures} = require("../../config");
*/ */
let currentDownloadProcess = null; let currentDownloadProcess = null;
/**
* Determines whether a URL targets a playlist.
*
* The detection is based on the presence of the YouTube
* playlist query parameter (`list`).
*
* @param {string} url - URL to inspect.
* @returns {boolean} True if the URL appears to be a playlist.
*/
function isPlaylistUrl(url) {
return url.includes("?list=") || url.includes("&list=");
}
/** /**
* Creates a dedicated download folder for a playlist. * Creates a dedicated download folder for a playlist.
* *
@@ -49,23 +37,23 @@ function isPlaylistUrl(url) {
*/ */
function createPlaylistFolder(basePath, playlistTitle) { function createPlaylistFolder(basePath, playlistTitle) {
const sanitizedTitle = playlistTitle const sanitizedTitle = playlistTitle
.replace(/[<>:"|?*]/g, '') .replace(/[<>:"|?*]/g, '')
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
.trim() .trim()
.substring(0, 200); .substring(0, 200);
const playlistPath = path.join(basePath, sanitizedTitle); const playlistPath = path.join(basePath, sanitizedTitle);
try { try {
if (!fs.existsSync(playlistPath)) { if (!fs.existsSync(playlistPath)) {
fs.mkdirSync(playlistPath, { recursive: true }); fs.mkdirSync(playlistPath, { recursive: true });
logger.info(`Playlist folder created: ${playlistPath}`); logger.info(`Playlist folder created: ${playlistPath}`);
return playlistPath; return playlistPath;
} }
let counter = 1; let counter = 1;
let newPath; let newPath;
while (counter <= 1000) { while (counter <= 1000) {
newPath = path.join(basePath, `${sanitizedTitle} ${counter}`); newPath = path.join(basePath, `${sanitizedTitle} ${counter}`);
if (!fs.existsSync(newPath)) { if (!fs.existsSync(newPath)) {
@@ -75,17 +63,17 @@ function createPlaylistFolder(basePath, playlistTitle) {
} }
counter++; counter++;
} }
logger.error(`Could not find available playlist folder after 1000 attempts`); logger.error(`Could not find available playlist folder after 1000 attempts`);
throw new Error("Unable to create playlist folder"); logger.error("Unable to create playlist folder");
} catch (err) { } catch (err) {
logger.warn(`Failed to create playlist folder with title "${sanitizedTitle}": ${err.message}`); logger.warn(`Failed to create playlist folder with title "${sanitizedTitle}": ${err.message}`);
// Fallback: create folder "Untitled Playlist X" // Fallback: create folder "Untitled Playlist X"
let counter = 1; let counter = 1;
let fallbackPath; let fallbackPath;
while (counter <= 1000) { while (counter <= 1000) {
fallbackPath = path.join(basePath, `Untitled Playlist ${counter}`); fallbackPath = path.join(basePath, `Untitled Playlist ${counter}`);
if (!fs.existsSync(fallbackPath)) { if (!fs.existsSync(fallbackPath)) {
@@ -100,7 +88,7 @@ function createPlaylistFolder(basePath, playlistTitle) {
} }
counter++; counter++;
} }
logger.error(`Could not create playlist folder after 1000 attempts`); logger.error(`Could not create playlist folder after 1000 attempts`);
throw new Error("Unable to create playlist folder"); throw new Error("Unable to create playlist folder");
} }
@@ -139,17 +127,17 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
const userConfig = reloadFeatures(); const userConfig = reloadFeatures();
logger.info(`CONFIG createPlaylistFolders: ${userConfig.createPlaylistFolders}`); logger.info(`CONFIG createPlaylistFolders: ${userConfig.createPlaylistFolders}`);
let outputFolder = options.outputFolder || defaultDownloadFolder; let outputFolder = options.outputFolder || defaultDownloadFolder;
// Normalize path and validate it's safe (within Users folder) // Normalize path and validate it's safe (within Users folder)
let safeOutputFolder = path.resolve(outputFolder); let safeOutputFolder = path.resolve(outputFolder);
if (!isSafePath(safeOutputFolder)) { if (!isSafePath(safeOutputFolder)) {
logger.warn(`Path not allowed, using default instead: ${safeOutputFolder}`); logger.warn(`Path not allowed, using default instead: ${safeOutputFolder}`);
safeOutputFolder = path.resolve(defaultDownloadFolder); safeOutputFolder = path.resolve(defaultDownloadFolder);
} }
// Create download folder if it doesn't exist // Create download folder if it doesn't exist
try { try {
fs.mkdirSync(safeOutputFolder, { recursive: true }); fs.mkdirSync(safeOutputFolder, { recursive: true });
@@ -160,7 +148,7 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
} }
// Détecte si c'est une playlist et crée un dossier approprié // Détecte si c'est une playlist et crée un dossier approprié
const isPlaylist = options.playlistTitle || isPlaylistUrl(options.url); const isPlaylist = options.playlistTitle || isUrlPlaylist(options.url);
if (isPlaylist && userConfig.createPlaylistFolders) { if (isPlaylist && userConfig.createPlaylistFolders) {
try { try {
@@ -196,13 +184,13 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
else reject(new Error(`YT-DLP failed with code : ${code}`)); else reject(new Error(`YT-DLP failed with code : ${code}`));
}); });
child.stdout.on("data", data => { child.stdout.on("data", data => {
data.toString().split("\n").forEach(line => { data.toString().split("\n").forEach(line => {
if (!line.trim()) return; if (!line.trim()) return;
logger.info(`[yt-dlp] ${line}`); logger.info(`[yt-dlp] ${line}`);
// Progress Bar // Progress Bar
if (line.startsWith("[download] Destination:")) { if (line.startsWith("[download] Destination:")) {
progressListeners.forEach(fn => fn("reset")); progressListeners.forEach(fn => fn("reset"));
stageListeners.forEach(fn => fn("Downloading...")); stageListeners.forEach(fn => fn("Downloading..."));
@@ -253,7 +241,7 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
data.toString().split("\n").forEach(line => line.trim() && logger.error(`[yt-dlp stderr] ${line}`)); data.toString().split("\n").forEach(line => line.trim() && logger.error(`[yt-dlp stderr] ${line}`));
}); });
}); });
} }
@@ -270,7 +258,7 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
function cancelDownload() { function cancelDownload() {
if (currentDownloadProcess) { if (currentDownloadProcess) {
logger.info("Cancelling download and killing all child processes..."); logger.info("Cancelling download and killing all child processes...");
// Force kill the process and all its children with SIGKILL // Force kill the process and all its children with SIGKILL
try { try {
// Try to kill with SIGKILL on Windows (process group) or Unix // Try to kill with SIGKILL on Windows (process group) or Unix
@@ -286,7 +274,7 @@ function cancelDownload() {
// Fallback to regular kill // Fallback to regular kill
currentDownloadProcess.kill('SIGKILL'); currentDownloadProcess.kill('SIGKILL');
} }
currentDownloadProcess = null; currentDownloadProcess = null;
logger.info("Download cancelled successfully"); logger.info("Download cancelled successfully");
return true; return true;

View File

@@ -1,6 +1,6 @@
const { execFile } = require("child_process"); const { execFile } = require("child_process");
const { userYtDlp, denoPath } = require("../helpers/path.helpers"); const { userYtDlp, denoPath } = require("../helpers/path.helpers");
const getUserBrowser = require("../helpers/getBrowser.helpers"); const {getUserBrowser} = require("../helpers/getBrowser.helpers");
const { logger } = require("../logger"); const { logger } = require("../logger");
/** /**
@@ -61,6 +61,7 @@ function fetchInfo(url) {
try { try {
const data = JSON.parse(stdout); const data = JSON.parse(stdout);
resolve(data); resolve(data);
} catch (e) { } catch (e) {
logger.error(`JSON Parsing Error: ${e.message}`); logger.error(`JSON Parsing Error: ${e.message}`);