mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-09-28 20:41:14 +02:00
Compare commits
5 Commits
1.6.0
...
1bff8376b4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bff8376b4 | ||
|
|
3f44c01bd7 | ||
|
|
d0bdd887e3 | ||
|
|
609ac2eadb | ||
|
|
c484d322bf |
26
config.js
26
config.js
@@ -7,7 +7,7 @@ const path = require("path");
|
||||
* Indicates whether the application is running in development mode.
|
||||
* Determined by Electron's packaging state.
|
||||
*/
|
||||
const devMode = !app.isPackaged;
|
||||
const devMode = process.env.NODE_ENV === "test" || !app?.isPackaged;
|
||||
|
||||
/**
|
||||
* Resolves the configuration file path depending on runtime environment.
|
||||
@@ -68,13 +68,30 @@ function loadFeatures() {
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads feature flags from disk.
|
||||
* Use this instead of the cached configFeatures snapshot
|
||||
* when you need the latest user settings at call time.
|
||||
*
|
||||
* @returns {Object} Fresh feature flags from config file
|
||||
*/
|
||||
function reloadFeatures() {
|
||||
try {
|
||||
const raw = fs.readFileSync(featuresPath, "utf-8");
|
||||
return JSON.parse(raw);
|
||||
} catch (err) {
|
||||
// Fallback to cached snapshot if file read fails
|
||||
return configFeatures;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory snapshot of application feature flags.
|
||||
*
|
||||
* Note: Changes to the config file are not automatically reflected.
|
||||
* A restart or reload is required.
|
||||
*/
|
||||
const configFeatures = loadFeatures();
|
||||
let configFeatures = loadFeatures();
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
@@ -102,6 +119,11 @@ module.exports = {
|
||||
*/
|
||||
configFeatures,
|
||||
|
||||
/**
|
||||
* Function to reload the config
|
||||
*/
|
||||
reloadFeatures,
|
||||
|
||||
/**
|
||||
* Path to the active configuration file
|
||||
*/
|
||||
|
||||
21
jest.config.js
Normal file
21
jest.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
testEnvironment: "node",
|
||||
testMatch: ["**/test/unit/**/*.test.js"],
|
||||
collectCoverageFrom: [
|
||||
"server/**/*.js",
|
||||
"app/**/*.js",
|
||||
"!app/autoUpdater.js",
|
||||
],
|
||||
|
||||
modulePathIgnorePatterns: [
|
||||
"<rootDir>/dist",
|
||||
"<rootDir>/release",
|
||||
"<rootDir>/build",
|
||||
],
|
||||
|
||||
watchPathIgnorePatterns: [
|
||||
"<rootDir>/dist",
|
||||
"<rootDir>/release",
|
||||
"<rootDir>/build",
|
||||
],
|
||||
};
|
||||
6086
package-lock.json
generated
6086
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -16,12 +16,16 @@
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron --trace-warnings .",
|
||||
"postinstall": "chmod +x resources/binaries/linux/* 2>/dev/null || true",
|
||||
|
||||
"build": "electron-builder",
|
||||
"build:win": "electron-builder --win",
|
||||
"build:linux": "electron-builder --linux",
|
||||
"build:rpm": "bash scripts/create-copr-package.sh",
|
||||
"build:all": "electron-builder -wl",
|
||||
|
||||
"test": "jest",
|
||||
"test:unit": "jest tests/unit",
|
||||
|
||||
"update": "npm update"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -38,8 +42,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/devtron": "^2.1.1",
|
||||
"@jest/globals": "^30.4.1",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"electron": "^41.2.0",
|
||||
"electron-builder": "^25.1.8"
|
||||
"electron-builder": "^26.15.3",
|
||||
"jest": "^30.4.2",
|
||||
"playwright": "^1.62.1"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.masteracnolo.freedomloader",
|
||||
@@ -91,7 +99,7 @@
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "build/app-icon.ico",
|
||||
"artifactName": "${productName}-Setup-${version}.${ext}",
|
||||
"artifactName": "Freedom-Loader-Setup-${version}.${ext}",
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "resources/binaries/win-32/yt-dlp.exe",
|
||||
|
||||
@@ -6,7 +6,7 @@ const { buildYtDlpArgs } = require("../helpers/buildArgs.helpers");
|
||||
const notify = require("../helpers/notify.helpers");
|
||||
const path = require("path");
|
||||
const { isSafePath } = require("../helpers/validation.helpers");
|
||||
const { configFeatures } = require("../../config.js");
|
||||
const {reloadFeatures} = require("../../config");
|
||||
|
||||
/**
|
||||
* Reference to the currently running yt-dlp process.
|
||||
@@ -135,7 +135,10 @@ function createPlaylistFolder(basePath, playlistTitle) {
|
||||
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.info(`CONFIG createPlaylistFolders: ${configFeatures.createPlaylistFolders}`);
|
||||
|
||||
const userConfig = reloadFeatures();
|
||||
|
||||
logger.info(`CONFIG createPlaylistFolders: ${userConfig.createPlaylistFolders}`);
|
||||
|
||||
let outputFolder = options.outputFolder || defaultDownloadFolder;
|
||||
|
||||
@@ -158,8 +161,8 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
|
||||
|
||||
// Détecte si c'est une playlist et crée un dossier approprié
|
||||
const isPlaylist = options.playlistTitle || isPlaylistUrl(options.url);
|
||||
|
||||
if (isPlaylist && configFeatures.createPlaylistFolders) {
|
||||
|
||||
if (isPlaylist && userConfig.createPlaylistFolders) {
|
||||
try {
|
||||
const playlistName = options.playlistTitle || "Untitled Playlist";
|
||||
safeOutputFolder = createPlaylistFolder(safeOutputFolder, playlistName);
|
||||
@@ -168,7 +171,7 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
|
||||
logger.error(`Failed to create playlist folder: ${err.message}`);
|
||||
return reject(err);
|
||||
}
|
||||
} else if (isPlaylist && !configFeatures.createPlaylistFolders) {
|
||||
} else if (isPlaylist && !userConfig.createPlaylistFolders) {
|
||||
logger.info(`Playlist detected but createPlaylistFolders is disabled, using base folder`);
|
||||
}
|
||||
|
||||
|
||||
182
test/unit/buildArgs.test.js
Normal file
182
test/unit/buildArgs.test.js
Normal file
@@ -0,0 +1,182 @@
|
||||
const path = require("path");
|
||||
|
||||
jest.mock("../../server/helpers/getBrowser.helpers.js", () =>
|
||||
jest.fn(() => "firefox")
|
||||
);
|
||||
|
||||
jest.mock("../../server/helpers/path.helpers.js", () => ({
|
||||
ffmpegPath: "/mock/ffmpeg",
|
||||
denoPath: "/mock/deno",
|
||||
}));
|
||||
|
||||
jest.mock("../../config.js", () => ({
|
||||
configFeatures: {
|
||||
autoUpdate: false,
|
||||
discordRPC: false,
|
||||
customTopBar: false,
|
||||
autoCheckInfo: false,
|
||||
addThumbnail: true,
|
||||
addMetadata: true,
|
||||
verboseLogs: false,
|
||||
autoDownloadPlaylist: false,
|
||||
customCodec: "h264",
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../server/logger.js", () => ({
|
||||
logger: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const { buildYtDlpArgs } = require("../../server/helpers/buildArgs.helpers");
|
||||
const { configFeatures } = require("../../config");
|
||||
const { logger } = require("../../server/logger");
|
||||
|
||||
describe("buildYtDlpArgs", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("builds default video arguments", () => {
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain("--cookies-from-browser");
|
||||
expect(args).toContain("firefox");
|
||||
|
||||
expect(args).toContain("--ffmpeg-location");
|
||||
expect(args).toContain("/mock/ffmpeg");
|
||||
|
||||
expect(args).toContain("--js-runtimes");
|
||||
expect(args).toContain("deno:/mock/deno");
|
||||
|
||||
expect(args).toContain("--merge-output");
|
||||
expect(args).toContain("mp4");
|
||||
|
||||
expect(args).toContain("-f");
|
||||
expect(args).toContain("bestvideo+bestaudio/best/mp4");
|
||||
|
||||
expect(args).toContain(
|
||||
path.join("/downloads", "%(title)s.%(ext)s")
|
||||
);
|
||||
|
||||
expect(args.at(-1)).toBe("https://youtube.com/watch?v=123");
|
||||
});
|
||||
|
||||
test("builds audio-only arguments", () => {
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: true,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain("--extract-audio");
|
||||
expect(args).toContain("--audio-format");
|
||||
expect(args).toContain("mp3");
|
||||
expect(args).toContain("bestaudio");
|
||||
|
||||
expect(args).not.toContain("--merge-output");
|
||||
});
|
||||
|
||||
test("uses requested quality", () => {
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "720",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain(
|
||||
"bestvideo[height<=720]+bestaudio/best[height<=720]/mp4"
|
||||
);
|
||||
});
|
||||
|
||||
test("falls back to best for unknown quality", () => {
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "unknown",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain("best");
|
||||
});
|
||||
|
||||
test("uses --no-playlist by default", () => {
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain("--no-playlist");
|
||||
expect(args).not.toContain("--yes-playlist");
|
||||
});
|
||||
|
||||
test("uses --yes-playlist when enabled", () => {
|
||||
configFeatures.autoDownloadPlaylist = true;
|
||||
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain("--yes-playlist");
|
||||
expect(args).not.toContain("--no-playlist");
|
||||
|
||||
configFeatures.autoDownloadPlaylist = false;
|
||||
});
|
||||
|
||||
test("embeds thumbnail and metadata when enabled", () => {
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain("--embed-thumbnail");
|
||||
expect(args).toContain("--add-metadata");
|
||||
});
|
||||
|
||||
test("logs codec validation", () => {
|
||||
buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith("Codec Valid: h264");
|
||||
});
|
||||
|
||||
test("falls back to h264 when codec is invalid", () => {
|
||||
configFeatures.customCodec = "invalid";
|
||||
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"Codec not valid: invalid. Using default codec"
|
||||
);
|
||||
|
||||
const sortIndex = args.indexOf("-S");
|
||||
expect(args[sortIndex + 1]).toContain("vcodec:h264");
|
||||
|
||||
configFeatures.customCodec = "h264";
|
||||
});
|
||||
});
|
||||
133
test/unit/notify.helpers.test.js
Normal file
133
test/unit/notify.helpers.test.js
Normal file
@@ -0,0 +1,133 @@
|
||||
const mockShow = jest.fn();
|
||||
const mockOn = jest.fn();
|
||||
const mockOpenPath = jest.fn();
|
||||
const mockOpenExternal = jest.fn();
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
Notification: jest.fn().mockImplementation(() => ({
|
||||
show: mockShow,
|
||||
on: mockOn,
|
||||
})),
|
||||
shell: {
|
||||
openPath: mockOpenPath,
|
||||
openExternal: mockOpenExternal,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../server/helpers/path.helpers", () => ({
|
||||
iconPaths: {
|
||||
confirm: "/icons/confirm.png",
|
||||
error: "/icons/error.png",
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../server/logger", () => ({
|
||||
logger: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const { Notification, shell } = require("electron");
|
||||
|
||||
const {
|
||||
notifyDownloadFinished,
|
||||
notifyCookiesBrowserError,
|
||||
notifyFirefoxBrowserMissing,
|
||||
} = require("../../server/helpers/notify.helpers");
|
||||
|
||||
describe("notify.helpers", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("notifyDownloadFinished", () => {
|
||||
test("does nothing when notifications are disabled", () => {
|
||||
notifyDownloadFinished("/downloads", false);
|
||||
|
||||
expect(Notification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does nothing when folder is missing", () => {
|
||||
notifyDownloadFinished(undefined, true);
|
||||
|
||||
expect(Notification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("creates and shows notification", () => {
|
||||
notifyDownloadFinished("/downloads");
|
||||
|
||||
expect(Notification).toHaveBeenCalledWith({
|
||||
title: "Freedom Loader",
|
||||
body: "Your download is complete, click here to open it.",
|
||||
icon: "/icons/confirm.png",
|
||||
});
|
||||
|
||||
expect(mockOn).toHaveBeenCalledWith(
|
||||
"click",
|
||||
expect.any(Function)
|
||||
);
|
||||
|
||||
expect(mockShow).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("opens folder when notification is clicked", () => {
|
||||
notifyDownloadFinished("/downloads");
|
||||
|
||||
const callback = mockOn.mock.calls[0][1];
|
||||
callback();
|
||||
|
||||
expect(shell.openPath).toHaveBeenCalledWith("/downloads");
|
||||
});
|
||||
});
|
||||
|
||||
describe("notifyCookiesBrowserError", () => {
|
||||
test("creates notification", () => {
|
||||
notifyCookiesBrowserError();
|
||||
|
||||
expect(Notification).toHaveBeenCalledWith({
|
||||
title: "Cookies Error",
|
||||
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.",
|
||||
icon: "/icons/error.png",
|
||||
});
|
||||
|
||||
expect(mockShow).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("opens tutorial when clicked", () => {
|
||||
notifyCookiesBrowserError();
|
||||
|
||||
const callback = mockOn.mock.calls[0][1];
|
||||
callback();
|
||||
|
||||
expect(shell.openExternal).toHaveBeenCalledWith(
|
||||
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("notifyFirefoxBrowserMissing", () => {
|
||||
test("creates notification", () => {
|
||||
notifyFirefoxBrowserMissing();
|
||||
|
||||
expect(Notification).toHaveBeenCalledWith({
|
||||
title: "Firefox Missing",
|
||||
body: "Firefox was not found on your system. Click here to follow the installation guide",
|
||||
icon: "/icons/error.png",
|
||||
});
|
||||
|
||||
expect(mockShow).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("opens installation guide when clicked", () => {
|
||||
notifyFirefoxBrowserMissing();
|
||||
|
||||
const callback = mockOn.mock.calls[0][1];
|
||||
callback();
|
||||
|
||||
expect(shell.openExternal).toHaveBeenCalledWith(
|
||||
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user