Compare commits

..

61 Commits

Author SHA1 Message Date
MasterAcnolo
2a9b4420de fix(tray): dynamically create/destroy tray icon on setting toggle
Previously, toggling the system tray in the settings updated the configuration but did not instantiate or destroy the tray immediately. This caused a desynchronization bug where the application could minimize to a non-existent tray, creating a ghost instance.

The `set-feature` IPC handler now intercepts the `systemTray` key to apply the state at runtime before persisting the config.
2026-08-14 22:11:56 +02:00
MasterAcnolo
e521747baa feat: custom signature in every file download, as a comment 2026-08-14 22:08:41 +02:00
MasterAcnolo
2d71449a10 fix: tray behaviror. App will shutdown gracefully 2026-08-14 22:08:23 +02:00
MasterAcnolo
0a152d4b10 fix: rename "listeners" in "progressListeners", that enhance readability 2026-08-14 22:05:12 +02:00
MasterAcnolo
676607b2c9 fix: disable systemTray by default. Could be reenable by default on a next release. 2026-08-14 21:39:13 +02:00
MasterAcnolo
def3ced215 update: package-lock.json 2026-08-14 17:22:31 +02:00
MasterAcnolo
febe8364cd fix: log session date format (W Sweden) 2026-08-14 17:22:07 +02:00
MasterAcnolo
25d3b6e8e7 fix: false warn about DownloadPath at startup 2026-08-14 17:18:28 +02:00
MasterAcnolo
7ab23d495c feat: system tray. Working on Windows, should be tested on Linux (GNOME) env. More features to systemTray coming soon 2026-08-13 21:46:22 +02:00
MasterAcnolo
331890f743 fix: missing config reference in main.js and tray.js 2026-08-13 19:10:36 +02:00
MasterAcnolo
e398f6866a feat: allow app to minimize on the systemTray. Could be disable on purpose. 2026-08-09 09:58:55 +02:00
MasterAcnolo
5f28aa502a fix: discord RPC on Linux, creating symlink between Discord and Freedom Loader at start if needed 2026-08-08 10:51:10 +02:00
MasterAcnolo
4a10ef2614 refactor: app display in front end. To make difference more obvious between bundled and not 2026-08-08 10:50:39 +02:00
MasterAcnolo
1296199f65 add: somes startup configuration for dev 2026-08-08 10:50:18 +02:00
MasterAcnolo
764b566699 update: bump version to 1.6.2 2026-08-08 10:12:21 +02:00
MasterAcnolo
0c568c1e8d fix: small fixes in README and update project structure 2026-08-08 10:12:06 +02:00
MasterAcnolo
dae4dd852f add: nodemon configuration for developpment env 2026-08-08 09:28:03 +02:00
MasterAcnolo
e5c5542750 add: nodemon configuration for developpment env 2026-08-08 09:28:01 +02:00
MasterAcnolo
9b2c6c9552 fix: release.sh version format 2026-08-07 23:28:24 +02:00
MasterAcnolo
871ebe2498 refactor: enhance log format by adding color when app is run in an IDE or a Terminal 2026-08-07 23:28:00 +02:00
Axel Nicolas
730069a3c6 Merge pull request #46 from MasterAcnolo/1.6.1
1.6.1
2026-08-07 22:27:33 +02:00
MasterAcnolo
7a0cbd7456 refactor: changed /info router from POST to GET 2026-08-07 21:13:12 +02:00
MasterAcnolo
5d676b45e7 refactor: moved "Auto Fetch Info" settings to advanced settings 2026-08-07 21:06:11 +02:00
MasterAcnolo
a7f3d84a7d fix: log start format and smalls coms fix 2026-08-07 21:04:28 +02:00
MasterAcnolo
fbce5e5b7b update: dependencies 2026-08-05 22:51:55 +02:00
MasterAcnolo
cc7cc38392 feat: automatic release creation with scripts 2026-08-05 22:51:31 +02:00
MasterAcnolo
6a9e5a063b refactor: notify.helpers.js logic. Add a timeout and a better notification management to avoid race condition on Unix Systems 2026-08-05 22:16:15 +02:00
MasterAcnolo
cf8c95711c refactor: notify.helpers.js logic. Add a timeout and a better notification management to avoid race condition on Unix Systems 2026-08-05 22:15:53 +02:00
MasterAcnolo
3683e4245f fix: enhance readme with keepPlaylistOrder information. And fix some typo to fix Linux Env 2026-08-05 22:10:44 +02:00
MasterAcnolo
01affd8ddf feat: Keep Playlist Order features. Add index before title if it's a playlist and we activate this feature. 2026-08-05 21:52:14 +02:00
MasterAcnolo
faba3f516b fix: unit test wrong folder 2026-08-05 21:49:37 +02:00
MasterAcnolo
c1d9448a2b fix: bump version to 1.6.1 and update electron to 43.3.0 2026-08-05 21:49:25 +02:00
MasterAcnolo
1bff8376b4 fix: force artifact name for exe 2026-08-04 22:04:20 +02:00
MasterAcnolo
3f44c01bd7 fix: refresh config at every download. Used by CreatePlaylistFolder 2026-08-04 22:03:55 +02:00
MasterAcnolo
d0bdd887e3 fix: exclude dist, release and build folder from jest config 2026-08-02 21:40:39 +02:00
MasterAcnolo
609ac2eadb fix: better dev mode comparaison, useful for tests 2026-08-02 09:25:39 +02:00
MasterAcnolo
c484d322bf feat: unit tests for buildArgs and Notify 2026-08-02 09:25:05 +02:00
Axel Nicolas
b321a1d164 Merge pull request #45 from MasterAcnolo/linux-version
1.6.0 - Linux version
2026-07-28 21:38:03 +02:00
MasterAcnolo
89e6fcf9bc fix: snap Store package.json arguments 2026-07-28 20:43:04 +02:00
MasterAcnolo
ba2f317551 fix: snap Store mention in SECURITY.md 2026-07-28 20:42:51 +02:00
MasterAcnolo
553e383c94 refactor: rpm package publish 2026-07-27 22:40:30 +02:00
MasterAcnolo
89cc0b85a3 refactor: rpm package publish 2026-07-27 22:40:22 +02:00
MasterAcnolo
abd0bf9006 refactor: rpm package publish 2026-07-27 22:40:19 +02:00
MasterAcnolo
653f73805e fix: enhance package.json and rename linux package to freedom-loader 2026-07-27 22:15:19 +02:00
Axel Nicolas
4f03c5876f Merge pull request #44 from MasterAcnolo/sync
Sync
2026-07-26 15:15:52 +02:00
MasterAcnolo
a5f05f32af fix: readme missing items and [!TIP] 2026-07-26 11:21:23 +02:00
MasterAcnolo
0d1b1e4919 refactor: security.md 2026-07-26 09:37:39 +02:00
MasterAcnolo
0e77a94ec7 feat: enhance logger by displaying systeminfo at start of the app 2026-07-26 09:37:27 +02:00
MasterAcnolo
7f481d20b2 sync: update dependencies and this is ready for windows 2026-07-25 12:49:36 +02:00
MasterAcnolo
1ff32ca021 sync: to work on windows 2026-07-25 11:32:48 +02:00
MasterAcnolo
90d8a9f65d fix: correct use of logger for path.helpers.js 2026-07-22 20:41:54 +02:00
MasterAcnolo
d5965c2ce0 fix: bring back theme features for Linux 2026-07-22 18:44:30 +02:00
MasterAcnolo
15f59a6093 fix: bring back theme features for Linux 2026-07-22 18:16:33 +02:00
MasterAcnolo
8b31436e25 feat: publish package to fedora copr, add script to automaticaly build and upload the file.
Enhance readme and fix auto updater issue
2026-06-29 10:44:17 +02:00
MasterAcnolo
7d5203001b add: BINARIES.md readme file. About how to install the binaries for dev mode 2026-06-28 21:49:53 +02:00
MasterAcnolo
60709c4fad fix: don't show dialog box if no previous version is available. 2026-06-28 21:48:24 +02:00
MasterAcnolo
2fb6509edc add: dark.zip and white.zip theme to the repo 2026-06-28 21:34:26 +02:00
MasterAcnolo
eebce83976 fix: small code quality and mismatch. 2026-06-28 21:34:00 +02:00
MasterAcnolo
a56a070bc8 feat: support Linux. Refactor resources architecture. Create build script
Currently, themes are disabled and firefox notification too.
2026-06-28 21:32:49 +02:00
MasterAcnolo
e1ef7032bb add: default theme applied in app. If no theme is there 2026-06-18 21:55:50 +02:00
MasterAcnolo
e7333b7b6b fix: missing enableHardwareAcceleration settings in readme 2026-06-10 21:43:38 +02:00
50 changed files with 6689 additions and 2028 deletions

27
.gitignore vendored
View File

@@ -1,15 +1,24 @@
# Dependencies
/node_modules /node_modules
/downloads
# Build Output Folder
/dist /dist
/out /out
/srpm-out
/ressources/ffmpeg.exe # Config JSON File. Create it and fill it if you want to have dev only settings
/ressources/ffprobe.exe
/ressources/deno.exe
/theme/**
config/config.dev.json config/config.dev.json
logs/*.json # IDE
logs/*.log /.idea
/.vscode
# Linux executable
ffmpeg
ffprobe
deno
# Windows Executable
ffmpeg.exe
ffprobe.exe
deno.exe

96
BINARIES.md Normal file
View File

@@ -0,0 +1,96 @@
# External binaries
This project depends on three external tools that are **not bundled in the repository**.
You must download them manually and place each binary in the correct folder before building or running the app locally.
---
## Required tools
| Tool | Role |
|---------|---------------------------------------------|
| yt-dlp | Video and audio download engine |
| ffmpeg | Media encoding, transcoding, muxing |
| ffprobe | Media file analysis and metadata extraction |
| deno | JavaScript / TypeScript runtime |
---
## Folder structure
Place the binaries exactly as shown below.
Do not rename them — the application resolves them by these exact names.
```
resources/
└── binaries/
├── linux/
│ ├── yt-dlp
│ ├── ffmpeg
│ ├── ffprobe
│ └── deno
└── win-32/
├── yt-dlp.exe
├── ffmpeg.exe
├── ffprobe.exe
└── deno.exe
```
---
## Download links
### yt-dlp
| Platform | URL |
|----------|----------------------------------------------------------------------|
| Windows | https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe |
| Linux | https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp |
Rename the downloaded file to `yt-dlp` (or `yt-dlp.exe` on Windows).
---
### ffmpeg and ffprobe
**Windows and Linux** — both binaries are included in the same archive.
| Platform | URL |
|-------------|-----------------------------------------------------------------------------------------------------------|
| Windows x64 | https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip |
| Linux x64 | https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz |
| Linux arm64 | https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linuxarm64-gpl.tar.xz |
Extract the archive and copy `ffmpeg` and `ffprobe` from the `bin/` subfolder.
---
### deno
| Platform | URL |
|-------------|----------------------------------------------------------------------------------------------|
| Windows x64 | https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip |
| Linux x64 | https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip |
| Linux arm64 | https://github.com/denoland/deno/releases/latest/download/deno-aarch64-unknown-linux-gnu.zip |
Each archive contains a single binary. Rename it to `deno` (or `deno.exe` on Windows).
---
## Permissions (Linux )
After placing the binaries, make them executable:
```bash
chmod +x resources/binaries/linux/yt-dlp
chmod +x resources/binaries/linux/ffmpeg
chmod +x resources/binaries/linux/ffprobe
chmod +x resources/binaries/linux/deno
```
---
## Notes
- These binaries are excluded from version control via `.gitignore`.
- Always use the latest stable release of each tool.

View File

@@ -105,7 +105,7 @@ Freedom-Loader/
│ ├── controller/ # Business logic │ ├── controller/ # Business logic
│ └── helpers/ # Utility functions │ └── helpers/ # Utility functions
├── public/ # Frontend (HTML, CSS, JS) ├── public/ # Frontend (HTML, CSS, JS)
└── ressources/ # Binaries (yt-dlp, ffmpeg, etc.) └── resources/ # Binaries (yt-dlp, ffmpeg, etc.)
``` ```
--- ---

202
README.md
View File

@@ -5,9 +5,9 @@
alt="Banner" alt="Banner"
style="width: 50%;"/></a> style="width: 50%;"/></a>
### **A clean, open-source multimedia downloader for Windows** ### **A clean, open-source multimedia downloader for Windows and Linux**
[![Release](https://img.shields.io/badge/Release-1.5.1-blue?style=for-the-badge)](https://github.com/MasterAcnolo/Freedom-Loader/releases) [![Release](https://img.shields.io/github/v/release/MasterAcnolo/Freedom-Loader?style=for-the-badge&color=blue)](https://github.com/MasterAcnolo/Freedom-Loader/releases)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-red.svg?style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-red.svg?style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0)
[![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Site/) [![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Site/)
[![Workshop](https://img.shields.io/badge/Worshop-Visit-0E05A1?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Workshop/) [![Workshop](https://img.shields.io/badge/Worshop-Visit-0E05A1?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Workshop/)
@@ -61,9 +61,9 @@ The primary goal is to make media downloading accessible to users who want offli
### User Interface ### User Interface
- **Custom window controls** - Frameless window with custom top bar (optional) - **Custom window controls** - Frameless window with custom top bar (optional)
- **Custom themes** — ZIP-based theme system with live preview - **Custom themes** - ZIP-based theme system with live preview
- **Theme Workshop** — web-based theme creator and browser - **Theme Workshop** - web-based theme creator and browser
- **Toast notifications** — in-app feedback system - **Toast notifications** - in-app feedback system
- **Real-time progress tracking** - Live download progress bar with network speed indicator - **Real-time progress tracking** - Live download progress bar with network speed indicator
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming - **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
- **Settings panel** - In-app configuration interface with live updates - **Settings panel** - In-app configuration interface with live updates
@@ -80,41 +80,89 @@ The primary goal is to make media downloading accessible to users who want offli
- **Rate limiting** - Express-based request throttling to prevent server overload - **Rate limiting** - Express-based request throttling to prevent server overload
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections - **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
- **System notifications** - Windows notifications on download completion (clickable to open folder) - **System notifications** - Windows notifications on download completion (clickable to open folder)
- **Splash screen** — animated loading screen on startup - **Splash screen** - animated loading screen on startup
- **Stop download** — ability to cancel in-progress downloads - **Stop download** - ability to cancel in-progress downloads
- **Playlist folders** — dedicated folder creation per playlist - **Playlist folders** - dedicated folder creation per playlist
- **Keep Playlist Order** - Add an Index before the title, could be useful for an album for example.
## Installation ## Installation
[![Fedora Copr](https://img.shields.io/badge/Fedora-Copr-294172?style=for-the-badge&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/masteracnolo/freedom-loader/)
[![Debian/Ubuntu](https://img.shields.io/badge/.deb-Debian%2FUbuntu-A81D33?style=for-the-badge&logo=debian&logoColor=white)](https://github.com/MasterAcnolo/Freedom-Loader/releases)
[![Snap Store](https://img.shields.io/badge/Snap-Store-82BEA0?style=for-the-badge&logo=snapcraft&logoColor=white)](https://snapcraft.io/freedom-loader)
[![AppImage](https://img.shields.io/badge/AppImage-Universal-1E88E5?style=for-the-badge&logo=linux&logoColor=white)](https://github.com/MasterAcnolo/Freedom-Loader/releases)
### Requirements ### Requirements
- **Operating System**: Windows 10 or later - **Windows**: Windows 10 or later
- **Browser**: Mozilla Firefox (required for cookie extraction) - **Linux**: any modern x86_64 distribution
- [Download Firefox](https://www.firefox.com) - **Browser**: Mozilla Firefox ([download](https://www.firefox.com)) required for cookie extraction, on both platforms
### Standard Installation ### Windows
1. Download the latest release from the [Releases page](https://github.com/MasterAcnolo/Freedom-Loader/releases) 1. Download the latest `.exe` from the [Releases page](https://github.com/MasterAcnolo/Freedom-Loader/releases)
2. Run the downloaded `.exe` installer 2. Run the installer (click "More info" → "Run anyway" if Windows Defender warns you)
3. If Windows Defender shows a warning, click "More info" then "Run anyway" 3. Follow the installation wizard
4. Follow the installation wizard
### Linux
#### Fedora (via Copr - recommended)
Installing via Copr lets `dnf` handle future updates automatically:
```bash
sudo dnf copr enable masteracnolo/freedom-loader
sudo dnf install freedom-loader
```
#### Debian / Ubuntu (.deb)
```bash
# Download the latest .deb from the Releases page, then:
sudo apt install ./freedom-loader_1.x.x_amd64.deb
```
#### Snap (any distro with snapd)
```bash
sudo snap install freedom-loader
```
#### RPM (Fedora/RHEL/openSUSE, without Copr)
```bash
# Download the latest .rpm from the Releases page, then:
sudo dnf install ./freedom-loader-1.x.y.x86_64.rpm
```
#### AppImage (any distro, no installation needed)
```bash
# Download the latest .AppImage from the Releases page, then:
chmod +x Freedom-Loader-1.x.y.AppImage
./Freedom-Loader-1.x.y.AppImage
```
Once installed via any method, launch Freedom Loader from your application menu, or from a terminal with:
```bash
freedom-loader
```
### File Locations ### File Locations
Downloaded files are stored in: | | Windows | Linux |
``` |---------------|---------------------------------------------------------|--------------------------------------|
C:\Users\[USERNAME]\Downloads\Freedom Loader | **Downloads** | `C:\Users\[USERNAME]\Downloads\Freedom Loader` | `~/Downloads/Freedom Loader` |
``` | **Logs** | `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\` | `~/.local/share/FreedomLoader/logs/` |
> [!NOTE]
> *You can change it whenever you want. Just click the "Edit" button in the UI.*
Application logs can be found at: > [!NOTE]
``` > The download folder can be changed anytime via the "Edit" button in the UI, on both platforms.
C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\
```
## Usage ## Usage
> [!TIP]
> On Linux, you can start Freedom Loader using `freedom-loader` command
### Basic Download ### Basic Download
1. Launch Freedom Loader 1. Launch Freedom Loader
@@ -131,14 +179,15 @@ The application supports various configuration options:
- Custom download directory selection - Custom download directory selection
- Discord Rich Presence activation - Discord Rich Presence activation
- Theme customization - Theme customization
- Log level configuration - Log level
- And somes to see in the list [Configuration Options](#configuration-options) available
### Troubleshooting ### Troubleshooting
If you encounter issues: If you encounter issues:
1. Restart the application 1. Restart the application
2. Check the logs in `AppData\Local\FreedomLoader\logs\` 2. Check the logs in `AppData\Local\FreedomLoader\logs\` or `~/.local/share/FreedomLoader/logs`
3. Open the developer console: `CTRL + SHIFT + I` → "Toggle Developer Tools" 3. Open the developer console: `CTRL + SHIFT + I` → "Toggle Developer Tools"
4. Create an issue on GitHub with logs and reproduction steps 4. Create an issue on GitHub with logs and reproduction steps
@@ -164,14 +213,22 @@ If you encounter issues:
Freedom Loader can be configured either through the settings panel in the UI or by directly editing the `config.json` file located in the application data directory. Freedom Loader can be configured either through the settings panel in the UI or by directly editing the `config.json` file located in the application data directory.
### Configuration File Location ### Configuration File Location
#### Windows
``` ```
%APPDATA%\Freedom Loader\config.json %APPDATA%\Freedom Loader\config.json
``` ```
#### Linux
```
~/.config/Freedom\ Loader/config.json
```
### Available Options ### Available Options
```json ```json
{ {
"autoUpdate": true, "autoUpdate": true,
"systemTray": true,
"discordRPC": true, "discordRPC": true,
"customTopBar": true, "customTopBar": true,
"autoCheckInfo": true, "autoCheckInfo": true,
@@ -179,36 +236,40 @@ Freedom Loader can be configured either through the settings panel in the UI or
"addMetadata": true, "addMetadata": true,
"verboseLogs": false, "verboseLogs": false,
"autoDownloadPlaylist": true, "autoDownloadPlaylist": true,
"keepPlaylistOrder": false,
"createPlaylistFolders": true, "createPlaylistFolders": true,
"customCodec": "h264", "customCodec": "h264",
"logSystem": true, "logSystem": true,
"outputTitleCheck": true, "outputTitleCheck": true,
"downloadSystem": true, "downloadSystem": true,
"notifySystem": true, "notifySystem": true,
"enableHardwareAcceleration": true,
"theme": "dark" "theme": "dark"
} }
``` ```
### Configuration Options ### Configuration Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
|--------|------|---------|-------------| |------------------------------|---------|----------|------------------------------------------------------------|
| `autoUpdate` | boolean | `true` | Enable automatic application updates | | `autoUpdate` | boolean | `true` | Enable automatic application updates |
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration | | `systemTray` | boolean | `true` | Allow app to minimize on close |
| `customTopBar` | boolean | `true` | Use custom application top bar | | `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste | | `customTopBar` | boolean | `true` | Use custom application top bar |
| `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files | | `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files | | `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files |
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging | | `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists | | `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
| `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist | | `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) | | `keepPlaylistOrder` | boolean | `false` | Add index before title, usefull for album |
| `logSystem` | boolean | `true` | Enable application logging | | `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist |
| `outputTitleCheck` | boolean | `true` | Verify output file titles | | `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
| `downloadSystem` | boolean | `true` | Enable download system | | `logSystem` | boolean | `true` | Enable application logging |
| `notifySystem` | boolean | `true` | Enable system notifications | | `outputTitleCheck` | boolean | `true` | Verify output file titles |
| `enableHardwareAcceleration` | boolean | `true` | Enable HArdware Acceleration | | `downloadSystem` | boolean | `true` | Enable download system |
| `theme` | string | `dark` | Current Theme Name | | `notifySystem` | boolean | `true` | Enable system notifications |
| `enableHardwareAcceleration` | boolean | `true` | Enable Hardware Acceleration |
| `theme` | string | `dark` | Current Theme Name |
### Supported Codecs ### Supported Codecs
@@ -221,7 +282,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
- `theora` - Theora (legacy open codec) - `theora` - Theora (legacy open codec)
> [!NOTE] > [!NOTE]
> Configuration changes may require an application restart to take effect. > Some configuration changes may require an application restart to take effect.
## Project Structure ## Project Structure
@@ -235,15 +296,21 @@ Freedom-Loader/
│ ├── pathValidator.js │ ├── pathValidator.js
│ ├── splashManager.js │ ├── splashManager.js
│ ├── themeManager.js │ ├── themeManager.js
│ ├── tray.js
│ ├── windowManager.js │ ├── windowManager.js
│ └── ytDlpUpdater.js │ └── ytDlpUpdater.js
├── build/ # Build resources and assets ├── build/ # Build resources and assets
│ ├── app-icon.ico
│ ├── app-icon.png
│ ├── banner.png
│ └── ...
├── config/ # Configuration files ├── config/ # Configuration files
│ ├── config.default.json │ ├── config.default.json
│ └── config.dev.json │ └── config.dev.json # Configuration Used when i dev mode
├── dist/ # Compiled executables (.exe, .AppImage, .deb, .rpm, .snap)
├── public/ # Frontend assets and UI ├── public/ # Frontend assets and UI
│ ├── index.html │ ├── index.html
│ ├── splash.html │ ├── splash.html # Splash Screen (Start of the application)
│ ├── assets/ │ ├── assets/
│ │ ├── icon/ # Application icons │ │ ├── icon/ # Application icons
│ │ └── logo/ # Logo assets │ │ └── logo/ # Logo assets
@@ -262,19 +329,8 @@ Freedom-Loader/
│ ├── styles.css │ ├── styles.css
│ ├── variables.css │ ├── variables.css
│ ├── components/ # Component-specific styles │ ├── components/ # Component-specific styles
│ │ ├── checkbox.css
│ │ ├── editpathbutton.css
│ │ ├── loader.css
│ │ ├── progressBar.css
│ │ └── toast.css
│ └── layout/ # Layout styles │ └── layout/ # Layout styles
│ ├── container.css ├── resources/ # Internal resources (binaries : yt-dlp, ffmpeg...)
│ ├── form.css
│ ├── header.css
│ ├── settingsPanel.css
│ ├── topbar.css
│ └── videoinfo.css
├── ressources/ # Internal resources (binaries)
├── server/ # Express backend server ├── server/ # Express backend server
│ ├── logger.js │ ├── logger.js
│ ├── server.js │ ├── server.js
@@ -298,16 +354,16 @@ Freedom-Loader/
├── theme/ # Theme system ├── theme/ # Theme system
│ ├── template.theme.json # Theme template │ ├── template.theme.json # Theme template
│ ├── Dark/ # Default dark theme │ ├── Dark/ # Default dark theme
│ │ └── dark.theme.json
│ └── Light/ # Default light theme │ └── Light/ # Default light theme
│ └── light.theme.json ├── BINARIES.md # External dependencies & binaries documentation
├── main.js # Electron main process entry point
├── preload.js # Electron preload script (IPC bridge)
├── config.js # Global configuration loader
├── package.json # Project metadata and dependencies
├── CODE_OF_CONDUCT.md # Community guidelines ├── CODE_OF_CONDUCT.md # Community guidelines
├── config.js # Global configuration loader
├── CONTRIBUTING.md # Contribution guidelines ├── CONTRIBUTING.md # Contribution guidelines
├── jest.config.js # Unit testing configuration
├── LICENSE # GPLv3 license ├── LICENSE # GPLv3 license
├── main.js # Electron main process entry point
├── package.json # Project metadata and dependencies
├── preload.js # Electron preload script (IPC bridge)
└── README.md # This file └── README.md # This file
``` ```
@@ -334,7 +390,7 @@ Freedom Loader includes a web-based theme creator available at [Freedom Loader W
### Installing a custom theme ### Installing a custom theme
1. Download a `.zip` theme file 1. Download a `.zip` theme file
2. Drop it in the `theme/` folder of your Freedom Loader installation 2. Drop it in the `theme/` folder of your Freedom Loader installation
3. Restart the app — your theme appears in the settings panel 3. Restart the app - your theme appears in the settings panel
## Technology Stack ## Technology Stack
@@ -385,7 +441,7 @@ npm run build
This project relies on several external binaries. This project relies on several external binaries.
Some of them are **not included** in the public repository and must be added manually. Some of them are **not included** in the public repository and must be added manually.
You must download the required binaries and place them in the `ressources` folder, using the exact filenames listed below. You must download the required binaries and place them in the `resources` folder, using the exact filenames listed below.
#### Required binaries #### Required binaries
@@ -406,7 +462,7 @@ You must download the required binaries and place them in the `ressources` folde
Final folder structure: Final folder structure:
``` ```
ressources/ resources/
├── deno.exe ├── deno.exe
├── ffmpeg.exe ├── ffmpeg.exe
├── ffprobe.exe ├── ffprobe.exe
@@ -422,7 +478,7 @@ ressources/
- Write clear commit messages - Write clear commit messages
- Test thoroughly before submitting changes - Test thoroughly before submitting changes
- Update documentation when adding features - Update documentation when adding features
- Maintain compatibility with Windows 10+ - Maintain compatibility with Windows 10+ and Linux
## Roadmap ## Roadmap
@@ -444,7 +500,7 @@ Use the GitHub Issues system and include:
- Clear description of the issue - Clear description of the issue
- Steps to reproduce - Steps to reproduce
- Expected vs actual behavior - Expected vs actual behavior
- Relevant logs from `AppData\Local\FreedomLoader\logs\` - Relevant logs from `AppData\Local\FreedomLoader\logs\` or `~/.local/share/FreedomLoader/logs/`
- Screenshots if applicable - Screenshots if applicable
### Feature Requests ### Feature Requests
@@ -484,7 +540,7 @@ You are free to use, modify, and redistribute this software under the terms of t
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine - [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine
- [FFmpeg](https://ffmpeg.org/) for media processing capabilities - [FFmpeg](https://ffmpeg.org/) for media processing capabilities
- [Deno](https://deno.com/) for helping me bypassing challenges - [Deno](https://deno.com/) for helping me to bypass challenges
- [Electron](https://www.electronjs.org/) for the desktop application framework - [Electron](https://www.electronjs.org/) for the desktop application framework
- The open-source community for continuous support and contributions - The open-source community for continuous support and contributions
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one - [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one

View File

@@ -2,15 +2,14 @@
## Supported Versions ## Supported Versions
We actively maintain and provide security updates for the following versions of Freedom Loader: Freedom Loader follows a **latest-only** support model. Security fixes are applied to the current release only — no backports to older versions.
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------------- | ------------------ |
| 1.4.x | :white_check_mark: | | Latest stable | :white_check_mark: |
| 1.3.x | :white_check_mark: | | Older versions | :x: |
| < 1.3.0 | :x: |
**Note**: We recommend always using the latest version to benefit from the most recent security patches and features. I recommend always running the latest version. The built-in auto-updater will notify you when a new release is available. Older versions will not receive security patches regardless of severity.
## Reporting a Vulnerability ## Reporting a Vulnerability
@@ -32,37 +31,40 @@ Instead, please report security issues privately by:
### What to Expect ### What to Expect
- **Acknowledgment**: We will acknowledge receipt of your report within 48 hours - **Acknowledgment**: I will acknowledge receipt of your report within 48 hours
- **Assessment**: We will assess the vulnerability and determine its severity - **Assessment**: I will assess the vulnerability and determine its severity
- **Updates**: We will keep you informed of our progress - **Updates**: I will keep you informed of our progress
- **Resolution**: We aim to release a fix within 7-14 days for critical vulnerabilities - **Resolution**: I aim to release a fix within 7-14 days for critical vulnerabilities
- **Credit**: We will credit you in the release notes (unless you prefer to remain anonymous) - **Credit**: I will credit you in the release notes (unless you prefer to remain anonymous)
## Security Best Practices for Users ## Security Best Practices for Users
When using Freedom Loader, we recommend: When using Freedom Loader, I recommend:
### General Security ### General Security
- **Keep Updated**: Always use the latest version of Freedom Loader - **Keep Updated**: Always use the latest version of Freedom Loader — older versions receive no security patches
- **Official Sources**: Download only from official releases on [GitHub](https://github.com/MasterAcnolo/Freedom-Loader/releases) - **Official Sources**: Download only from official sources:
- **Verify Downloads**: Check that installers are properly signed (Windows SmartScreen may show warnings for new releases) - [GitHub Releases](https://github.com/MasterAcnolo/Freedom-Loader/releases)
- [Fedora COPR](https://copr.fedorainfracloud.org/coprs/masteracnolo/freedom-loader/)
- [Snap Store](https://snapcraft.io/freedom-loader)
- **Verify Downloads**: Check that installers are from one of the above sources
- **Antivirus**: Keep your antivirus software up to date - **Antivirus**: Keep your antivirus software up to date
### Configuration Security ### Configuration Security
- **Download Path**: Only set download paths within your user directory (`C:\Users\[USERNAME]\...`) - **Download Path**: Only set download paths within your user directory
- **Cookies**: Be aware that Firefox cookies are used for authentication—keep Firefox secure - **Cookies**: Be aware that Firefox cookies are used for authentication — keep Firefox secure
- **Logs**: Logs may contain sensitive information—avoid sharing them publicly without review - **Logs**: Logs may contain sensitive information such as URLs or local paths — avoid sharing them publicly without review
### Privacy Considerations ### Privacy Considerations
Freedom Loader respects your privacy: Freedom Loader respects your privacy:
- **No Data Collection**: We don't collect, store, or transmit your personal data - **No Data Collection**: I don't collect, store, or transmit your personal data
- **No Telemetry**: No usage tracking or analytics - **No Telemetry**: No usage tracking or analytics
- **Local Operation**: All downloads are processed locally on your machine - **Local Operation**: All downloads are processed locally on your machine
- **Optional Features**: Discord RPC is optional and can be disabled - **Optional Features**: Discord RPC is optional and can be disabled in settings
## Known Security Considerations ## Known Security Considerations
@@ -71,16 +73,16 @@ Freedom Loader respects your privacy:
Freedom Loader accesses Firefox cookies to download protected content. This is: Freedom Loader accesses Firefox cookies to download protected content. This is:
- **By Design**: Required for age-restricted or member-only content - **By Design**: Required for age-restricted or member-only content
- **Local Only**: Cookies are read locally and never transmitted - **Local Only**: Cookies are read locally and never transmitted outside your machine
- **User Controlled**: You can control what content you download - **User Controlled**: You control what content you download
### Native Dependencies ### Native Dependencies
Freedom Loader bundles native binaries: Freedom Loader bundles the following official binaries:
- **yt-dlp**: Official builds from [yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp) - **yt-dlp**: Official builds from [yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp) — auto-updated on each launch
- **FFmpeg**: Official builds from [FFmpeg.org](https://ffmpeg.org/) - **FFmpeg**: Official builds from [FFmpeg.org](https://ffmpeg.org/)
- **Deno**: Official builds from [Deno.land](https://deno.land/) - **Deno**: Official builds from [Deno.com](https://deno.com/)
These dependencies are verified and updated regularly. These dependencies are verified and updated regularly.
@@ -91,13 +93,13 @@ New releases may trigger Windows Defender warnings because:
- The application is not yet widely distributed - The application is not yet widely distributed
- Code signing certificates are expensive for open-source projects - Code signing certificates are expensive for open-source projects
This is expected behavior for new releases. The warning will decrease as more users download the software. This is expected behavior for new releases and will diminish as more users download the software.
## Vulnerability Disclosure Policy ## Vulnerability Disclosure Policy
### Our Commitment ### Our Commitment
We are committed to: I am committed to:
- Responding promptly to security reports - Responding promptly to security reports
- Working with security researchers to verify and address issues - Working with security researchers to verify and address issues
@@ -107,9 +109,9 @@ We are committed to:
### Disclosure Timeline ### Disclosure Timeline
1. **Day 0**: Vulnerability reported privately 1. **Day 0**: Vulnerability reported privately
2. **Day 1-2**: Acknowledgment sent to reporter 2. **Day 1–2**: Acknowledgment sent to reporter
3. **Day 3-7**: Vulnerability assessed and fix developed 3. **Day 3–7**: Vulnerability assessed and fix developed
4. **Day 7-14**: Fix released (critical vulnerabilities prioritized) 4. **Day 7–14**: Fix released (critical vulnerabilities prioritized)
5. **Day 14+**: Public disclosure (coordinated with reporter) 5. **Day 14+**: Public disclosure (coordinated with reporter)
### Scope ### Scope
@@ -129,16 +131,16 @@ The following are generally not considered security vulnerabilities:
- Issues requiring physical access to the user's machine - Issues requiring physical access to the user's machine
- Social engineering attacks - Social engineering attacks
- Denial of service against third-party services - Denial of service against third-party services
- Issues in third-party dependencies (report to upstream projects) - Issues in third-party dependencies (report to upstream projects directly)
- Missing security headers on local HTTP server (no remote access) - Missing security headers on the local HTTP server (not exposed remotely)
## Security Updates ## Security Updates
Security updates are released as: Security updates are released as:
- **Patch Releases**: For critical security fixes (e.g., 1.4.1 → 1.4.2) - **Patch Releases**: For critical security fixes (e.g., 1.6.0 → 1.6.1)
- **Automatic Updates**: Users are notified via the built-in update system - **Automatic Updates**: Users are notified via the built-in update system
- **Release Notes**: Security fixes are clearly marked in changelog - **Release Notes**: Security fixes are clearly marked in the changelog
## Additional Resources ## Additional Resources
@@ -150,6 +152,7 @@ Security updates are released as:
For security-related questions or concerns: For security-related questions or concerns:
- **Email**: masteracnolo25@gmail.com
- **GitHub**: [@MasterAcnolo](https://github.com/MasterAcnolo) - **GitHub**: [@MasterAcnolo](https://github.com/MasterAcnolo)
--- ---

View File

@@ -15,7 +15,7 @@ const { logger } = require("../server/logger");
/** /**
* Configures autoUpdater behavior: * Configures autoUpdater behavior:
* - disables automatic download * - disables automatic download
* - disables automatic install on quit * - disables automatic installation on quit
* (manual user-controlled update flow) * (manual user-controlled update flow)
*/ */
autoUpdater.autoDownload = false; autoUpdater.autoDownload = false;
@@ -51,7 +51,7 @@ function initAutoUpdater(mainWindow) {
}); });
if (response === 0) { if (response === 0) {
autoUpdater.downloadUpdate(); await autoUpdater.downloadUpdate();
} else { } else {
mainWindow?.webContents.executeJavaScript( mainWindow?.webContents.executeJavaScript(
`window.showUpdateBadge && window.showUpdateBadge("${info.version}")` `window.showUpdateBadge && window.showUpdateBadge("${info.version}")`
@@ -96,8 +96,20 @@ function initAutoUpdater(mainWindow) {
* Logs failure and displays an error dialog to user. * Logs failure and displays an error dialog to user.
*/ */
autoUpdater.on("error", (err) => { autoUpdater.on("error", (err) => {
logger.error("Auto update error:", err.message); const msg = err?.message || "";
dialog.showErrorBox("Update Error", err.message);
/**
* If no update is available, I put this because there is no Linux version before 1.6.0
* @type {boolean}
*/
const isNoUpdateAvailable = /404/.test(msg) || /Cannot find latest.*\.yml/i.test(msg);
if (isNoUpdateAvailable) {
logger.warn("Auto update: no update metadata found (probably no previous release), ignoring", msg);
return;
}
logger.error("Auto update error:", msg);
}); });
checkForUpdates(); checkForUpdates();
@@ -108,6 +120,14 @@ function initAutoUpdater(mainWindow) {
* Separated from init for reusability and testability. * Separated from init for reusability and testability.
*/ */
async function checkForUpdates() { async function checkForUpdates() {
if (process.env.SNAP) {
logger.info("Running as Snap, update managed by snapd, skipping electron-updater");
return;
}
if (!require("electron").app.isPackaged) return ;
try { try {
await autoUpdater.checkForUpdates(); await autoUpdater.checkForUpdates();
} catch (err) { } catch (err) {

View File

@@ -25,15 +25,19 @@ function checkNativeDependencies() {
*/ */
const { binaryPaths } = require("../server/helpers/path.helpers"); const { binaryPaths } = require("../server/helpers/path.helpers");
const isWindows = process.platform === "win32";
const addExe = isWindows ? ".exe" : "";
/** /**
* List of required external executables used by the application runtime. * List of required external executables used by the application runtime.
* Each entry defines a binary name and its resolved absolute path. * Each entry defines a binary name and its resolved absolute path.
*/ */
const deps = [ const deps = [
{ name: "yt-dlp.exe", path: binaryPaths.ytDlp }, { name: `yt-dlp${addExe}`, path: binaryPaths.ytDlp },
{ name: "ffmpeg.exe", path: binaryPaths.ffmpeg }, { name: `ffmpeg${addExe}`, path: binaryPaths.ffmpeg },
{ name: "ffprobe.exe", path: binaryPaths.ffprobe }, { name: `ffprobe${addExe}`, path: binaryPaths.ffprobe },
{ name: "deno.exe", path: binaryPaths.deno }, { name: `deno${addExe}`, path: binaryPaths.deno },
]; ];
const missing = deps.filter(d => !fs.existsSync(d.path)); const missing = deps.filter(d => !fs.existsSync(d.path));
@@ -52,7 +56,7 @@ function checkNativeDependencies() {
app.whenReady().then(() => { app.whenReady().then(() => {
dialog.showErrorBox( dialog.showErrorBox(
"Missing dependencies", "Missing dependencies",
`The following files are missing in the 'ressources' folder:\n${list}\n\nThe application will now exit. Try to reinstall.` `The following files are missing in the 'resources' folder:\n${list}\n\nThe application will now exit. Try to reinstall.`
); );
app.quit(); app.quit();
}); });

View File

@@ -1,6 +1,8 @@
const config = require('../config'); const config = require('../config');
const RPC = require("discord-rpc"); const RPC = require("discord-rpc");
const { logger } = require("../server/logger"); const { logger } = require("../server/logger");
const fs = require("fs");
const path = require("path");
/** /**
* Discord Application Client ID used to authenticate the RPC connection. * Discord Application Client ID used to authenticate the RPC connection.
@@ -20,6 +22,51 @@ const rpc = new RPC.Client({ transport: "ipc" });
*/ */
let intervalId; let intervalId;
/**
* Automagically fixes Discord RPC on Linux for Sandboxed versions (Flatpak & Snap).
* It creates a symlink from the sandbox IPC socket to the standard system location.
*
* @returns {void}
*/
function fixLinuxDiscordRPC() {
// Only execute on Linux
if (process.platform !== "linux") return;
/**
* Get the user's runtime directory (e.g., /run/user/1000).
* @type {string}
*/
const xdgDir = process.env.XDG_RUNTIME_DIR || `/run/user/${process.getuid()}`;
const standardSocket = path.join(xdgDir, "discord-ipc-0");
// If the standard socket already exists (Native Discord), do nothing.
if (fs.existsSync(standardSocket)) return;
/**
* Paths for known sandboxed Discord IPC sockets.
*/
const flatpakSocket = path.join(xdgDir, "app/com.discordapp.Discord/discord-ipc-0");
const snapSocket = path.join(xdgDir, "snap.discord/discord-ipc-0");
let targetSocket = null;
if (fs.existsSync(flatpakSocket)) {
targetSocket = flatpakSocket;
} else if (fs.existsSync(snapSocket)) {
targetSocket = snapSocket;
}
// If a sandboxed socket is found, create the symlink stealthily.
if (targetSocket) {
try {
fs.symlinkSync(targetSocket, standardSocket);
logger.info(`Discord RPC bridge created automatically targeting: ${targetSocket}`);
} catch (err) {
logger.warn(`Failed to create Discord RPC bridge: ${err.message}`);
}
}
}
/** /**
* Initializes Discord Rich Presence (RPC) connection. * Initializes Discord Rich Presence (RPC) connection.
* *
@@ -32,6 +79,11 @@ let intervalId;
*/ */
function startRPC() { function startRPC() {
/**
* Patch that create Simlinks for Linux environment;
*/
fixLinuxDiscordRPC();
/** /**
* Rich Presence payload describing current application state * Rich Presence payload describing current application state
* shown in Discord user profile. * shown in Discord user profile.
@@ -60,28 +112,30 @@ function startRPC() {
* *
* Responsibilities: * Responsibilities:
* - Clear any running intervals * - Clear any running intervals
* - Remove current activity from Discord * - Remove current activity from Discord if connected
* - Destroy RPC transport connection * - Destroy RPC transport connection
* - Handle cleanup errors safely * - Handle cleanup errors safely
*/ */
async function stopRPC(){ async function stopRPC() {
if (rpc) {
try { try {
if (intervalId) clearInterval(intervalId); if (intervalId) clearInterval(intervalId);
/** /**
* Ensures RPC connection exists before attempting cleanup. * Ensures RPC connection AND the underlying socket exist
* before attempting to clear activity to prevent crash.
*/ */
if (rpc && rpc.transport) { if (rpc.transport && rpc.transport.socket) {
await rpc.clearActivity() await rpc.clearActivity();
await rpc.destroy();
} else{
logger.error("Not Able to close RPC")
} }
await rpc.destroy();
} catch (err) { } catch (err) {
logger.error("Error while closing the RPC:", err); logger.error("Error while closing the RPC:", err);
} }
} }
}
module.exports = { startRPC, stopRPC}; module.exports = { startRPC, stopRPC};

View File

@@ -12,13 +12,13 @@
const { ipcMain, dialog, shell } = require("electron"); const { ipcMain, dialog, shell } = require("electron");
const fs = require("fs"); const fs = require("fs");
const path = require("path");
const { logger, logDir } = require("../server/logger"); const { logger, logDir } = require("../server/logger");
const { configFeatures, featuresPath } = require("../config"); const { configFeatures, featuresPath } = 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");
const { userThemesPath } = require("../server/helpers/path.helpers"); const { userThemesPath } = require("../server/helpers/path.helpers");
const { createSystemTray, destroyTray } = require("./tray");
/** /**
* Security whitelist for feature flags that can be modified at runtime. * Security whitelist for feature flags that can be modified at runtime.
@@ -28,6 +28,7 @@ const { userThemesPath } = require("../server/helpers/path.helpers");
*/ */
const FEATURE_WHITELIST = new Set([ const FEATURE_WHITELIST = new Set([
"autoUpdate", "autoUpdate",
"systemTray",
"discordRPC", "discordRPC",
"customTopBar", "customTopBar",
"autoCheckInfo", "autoCheckInfo",
@@ -35,6 +36,7 @@ const FEATURE_WHITELIST = new Set([
"addMetadata", "addMetadata",
"verboseLogs", "verboseLogs",
"autoDownloadPlaylist", "autoDownloadPlaylist",
"keepPlaylistOrder",
"customCodec", "customCodec",
"theme", "theme",
"createPlaylistFolders", "createPlaylistFolders",
@@ -213,6 +215,21 @@ function registerIpcHandlers(getMainWindow) {
return true; return true;
} }
/**
* Dynamically intercepts and applies System Tray state changes at runtime.
* Instantiates or destroys the tray icon immediately to prevent window
* lifecycle desynchronization (e.g., minimizing a window to a non-existent tray).
*/
if (key === "systemTray") {
if (value === true) {
logger.info("System Tray enabled dynamically.");
createSystemTray(getMainWindow(), config.devMode);
} else {
logger.info("System Tray disabled dynamically.");
destroyTray();
}
}
configFeatures[key] = value; configFeatures[key] = value;
fs.writeFileSync( fs.writeFileSync(

View File

@@ -56,26 +56,30 @@ function validateDownloadPath(userPath) {
if (!userPath) return getDefaultDownloadPath(); if (!userPath) return getDefaultDownloadPath();
try { try {
const absolutePath = path.resolve(userPath);
/** if (!fs.existsSync(absolutePath)) {
* Resolves filesystem symlinks to ensure canonical absolute path. fs.mkdirSync(absolutePath, { recursive: true });
* Required to prevent path traversal or alias bypass. logger.info(`Download folder created: ${absolutePath}`);
*/ }
const resolved = fs.realpathSync(path.resolve(userPath));
if (!isSafePath(resolved)) { const real = fs.realpathSync(absolutePath);
throw new Error("Path not allowed: system folders are blocked!");
if (!isSafePath(real)) {
throw new Error("Path not allowed: system folders are blocked!");
}
return real;
} }
return resolved;
}
catch (err) {
/** catch (err) {
* Handles invalid filesystem paths, permissions issues, or resolution failures. /**
* Logs diagnostic information before propagating a sanitized error. * Handles invalid filesystem paths, permissions issues, or resolution failures.
*/ * Logs diagnostic information before propagating a sanitized error.
logger.error(`Invalid download path: ${userPath} — ${err.message}`); */
throw new Error(`Invalid or inaccessible path: ${err.message}`); logger.error(`Invalid download path: ${userPath} — ${err.message}`);
} throw new Error(`Invalid or inaccessible path: ${err.message}`);
}
} }
module.exports = { validateDownloadPath, getDefaultDownloadPath }; module.exports = { validateDownloadPath, getDefaultDownloadPath };

View File

@@ -270,7 +270,6 @@ async function loadThemesFromFolder() {
if (fs.statSync(filePath).isDirectory()) { if (fs.statSync(filePath).isDirectory()) {
const theme = await loadThemeFromFolder(filePath, themeId); const theme = await loadThemeFromFolder(filePath, themeId);
if (theme) themes.push(theme); if (theme) themes.push(theme);
continue;
} }
} }

108
app/tray.js Normal file
View File

@@ -0,0 +1,108 @@
const {app, Menu, Tray, nativeImage} = require("electron");
const path = require("path");
const {logger} = require("../server/logger"); // Ajuste le chemin si besoin
const fs = require("fs");
/**
* Global reference to the Tray instance to prevent garbage collection.
* @type {Tray | null}
*/
let tray = null;
/**
* Creates and configures the cross-platform System Tray.
*
* Responsibilities:
* - Loads the appropriate icon format based on the operating system.
* - Builds the right-click context menu.
* - 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.
* @returns {Tray} The created Tray instance.
*/
function createSystemTray(mainWindow, devMode) {
// Prevent creating multiple tray instances
if (tray) return tray;
const isWin = process.platform === "win32";
const iconName = isWin ? "app-icon.ico" : "app-icon-64x64.png";
// Attempt to load the icon from the local build directory first
let iconPath = path.join(__dirname, "..", "build", iconName);
// Fallback to the extracted resources path if running in production
if (!devMode && !fs.existsSync(iconPath)) {
iconPath = path.join(process.resourcesPath, iconName);
}
if (!fs.existsSync(iconPath)) {
logger.error(`System Tray icon not found at resolved path: ${iconPath}`);
} else {
logger.info("System Tray icon resolved successfully.");
}
const icon = nativeImage.createFromPath(iconPath);
if (icon.isEmpty()) {
logger.error("System Tray icon file was found but could not be decoded by Electron (image is empty).");
}
tray = new Tray(icon);
tray.setToolTip("Freedom Loader");
/**
* System Tray Context Menu (Right-click action).
*/
const contextMenu = Menu.buildFromTemplate([
{
label: "Show Freedom Loader",
click: () => {
mainWindow.show();
},
},
{ type: "separator" },
{
label: "Quit",
click: () => {
/**
* Flags the application for a deliberate exit,
* bypassing the default "minimize to tray" behavior.
*/
app.isQuitting = true;
app.quit();
},
},
]);
tray.setContextMenu(contextMenu);
/**
* Left-click behavior: Toggles main window visibility.
* but it remains standard practice for Windows and Linux environments.
*/
tray.on("click", () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
mainWindow.focus();
}
});
return tray;
}
/**
* Safely destroys the tray icon to prevent OS-level crashes (especially on Linux/Wayland).
* Should be called right before the application quits.
*/
function destroyTray() {
if (tray && !tray.isDestroyed()) {
tray.destroy();
tray = null;
logger.info("System Tray destroyed cleanly.");
}
}
module.exports = {createSystemTray, destroyTray};

View File

@@ -81,6 +81,19 @@ async function createMainWindow() {
throw err; throw err;
} }
/**
* Intercepts the window close event.
* If the app is not explicitly quitting (via Tray menu),
* cancels the destruction and hides the window instead.
*/
mainWindow.on('close', (event) => {
if (!app.isQuitting && config.systemTray ) {
event.preventDefault();
mainWindow.hide();
return false;
}
});
/** /**
* Cleans up window reference when the main window is closed. * Cleans up window reference when the main window is closed.
* Prevents memory leaks and allows recreation if needed. * Prevents memory leaks and allows recreation if needed.

BIN
build/app-icon-64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
build/app-icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

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 = !app.isPackaged; const devMode = process.env.NODE_ENV === "test" || !app?.isPackaged;
/** /**
* Resolves the configuration file path depending on runtime environment. * Resolves the configuration file path depending on runtime environment.
@@ -68,13 +68,30 @@ function loadFeatures() {
return JSON.parse(raw); 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. * In-memory snapshot of application feature flags.
* *
* Note: Changes to the config file are not automatically reflected. * Note: Changes to the config file are not automatically reflected.
* A restart or reload is required. * A restart or reload is required.
*/ */
const configFeatures = loadFeatures(); let configFeatures = loadFeatures();
module.exports = { module.exports = {
/** /**
@@ -102,6 +119,11 @@ module.exports = {
*/ */
configFeatures, configFeatures,
/**
* Function to reload the config
*/
reloadFeatures,
/** /**
* Path to the active configuration file * Path to the active configuration file
*/ */

View File

@@ -1,5 +1,6 @@
{ {
"autoUpdate": true, "autoUpdate": true,
"systemTray": false,
"discordRPC": true, "discordRPC": true,
"customTopBar": true, "customTopBar": true,
"autoCheckInfo": true, "autoCheckInfo": true,
@@ -7,6 +8,7 @@
"addMetadata": true, "addMetadata": true,
"verboseLogs": false, "verboseLogs": false,
"autoDownloadPlaylist": true, "autoDownloadPlaylist": true,
"keepPlaylistOrder": false,
"createPlaylistFolders": true, "createPlaylistFolders": true,
"customCodec": "h264", "customCodec": "h264",
"logSystem": true, "logSystem": true,

21
jest.config.js Normal file
View 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",
],
};

61
main.js
View File

@@ -37,7 +37,7 @@ app.setAppUserModelId("com.masteracnolo.freedomloader");
/** /**
* Load the app dependencies for hardware choice * Load the app dependencies for hardware choice
*/ */
const { logger, logSessionStart, logSessionEnd } = require("./server/logger"); const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
const { configFeatures } = require("./config"); const { configFeatures } = require("./config");
/** /**
@@ -79,8 +79,22 @@ 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 } = require("./server/helpers/path.helpers"); const { userThemesPath, initUserThemes, isWindows, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
const {createSystemTray, destroyTray} = require("./app/tray");
/**
* Global flag indicating if the application is intentionally shutting down.
* Used across the app to bypass the "minimize to tray on close" behavior.
* @type {boolean}
*/
app.isQuitting = false;
/**
* State flag to track the asynchronous cleanup process during shutdown.
* Prevents the application from exiting before services (RPC, Tray, Logs) are properly closed.
* @type {boolean}
*/
let isCleanedUp = false;
/** /**
* If another instance want to run * If another instance want to run
@@ -99,7 +113,7 @@ app.on("second-instance", () => {
}); });
app.whenReady().then(async () => { app.whenReady().then(async () => {
logSessionStart(); logSessionStart(logDir, defaultDownloadFolder);
createSplashWindow(); createSplashWindow();
@@ -116,6 +130,9 @@ app.whenReady().then(async () => {
registerIpcHandlers(getMainWindow); registerIpcHandlers(getMainWindow);
setSplashProgress(2); // Loading themes setSplashProgress(2); // Loading themes
validateBinaries();
initUserThemes(); initUserThemes();
await initThemes(userThemesPath); await initThemes(userThemesPath);
@@ -124,6 +141,11 @@ app.whenReady().then(async () => {
closeSplashWindow(); closeSplashWindow();
getMainWindow().show(); getMainWindow().show();
if (configFeatures.systemTray) {
createSystemTray(getMainWindow(), config.devMode);
}
if (configFeatures.discordRPC) startRPC(); if (configFeatures.discordRPC) startRPC();
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow()); if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
@@ -135,12 +157,33 @@ app.whenReady().then(async () => {
}); });
app.on("window-all-closed", () => { app.on("window-all-closed", () => {
logger.info("Shutting down..."); if (app.isQuitting || !configFeatures.systemTray) {
app.quit(); logger.info("Shutting down...");
app.quit();
} else if (process.platform !== "darwin") {
logger.info("Main window closed, app running in background (Tray).");
}
}); });
app.on("before-quit", async () => { app.on("before-quit", (event) => {
await stopRPC(); app.isQuitting = true;
logger.info("All services stopped. Have a nice day!");
logSessionEnd(); if (!isCleanedUp) {
event.preventDefault();
(async () => {
try {
destroyTray();
await stopRPC();
} catch (err) {
logger.error("Error during cleanup:", err);
} finally {
logger.info("All services stopped. Have a nice day!");
logSessionEnd();
isCleanedUp = true;
app.quit();
}
})();
}
}); });

6143
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,39 @@
{ {
"name": "freedom-loader", "name": "freedom-loader",
"productName": "Freedom Loader", "productName": "Freedom Loader",
"version": "1.5.1", "version": "1.6.2-preview",
"author": "MasterAcnolo", "author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
"description": "Freedom Loader", "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/",
"repository": {
"type": "git",
"url": "https://github.com/MasterAcnolo/Freedom-Loader.git"
},
"license": "GPL-3.0-only",
"bugs": {
"url": "https://github.com/MasterAcnolo/Freedom-Loader/issues"
},
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"start": "electron --trace-warnings .", "start": "electron --no-warnings .",
"start:warn": "electron --trace-warnings .",
"start:debug": "WAYLAND_DEBUG=1 ELECTRON_ENABLE_LOGGING=1 ELECTRON_ENABLE_STACK_DUMPING=1 electron --enable-logging --v=1 .",
"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: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:linux": "electron-builder --linux",
"build:rpm": "bash scripts/create-copr-package.sh",
"build:all": "electron-builder -wl",
"release": "bash scripts/release.sh",
"release:publish": "bash scripts/release.sh --publish",
"release:dry-run": "bash scripts/release.sh --dry-run",
"test": "jest",
"test:unit": "jest test/unit",
"update": "npm update" "update": "npm update"
}, },
"dependencies": { "dependencies": {
@@ -23,10 +49,14 @@
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"electron": "^41.2.0", "@electron/devtron": "^2.1.1",
"electron-builder": "^26.8.1", "@jest/globals": "^30.4.1",
"@electron/devtron": "^2.1.1" "@playwright/test": "^1.62.1",
"electron": "^43.3.0",
"electron-builder": "^26.15.3",
"jest": "^30.4.2",
"nodemon": "^3.1.14",
"playwright": "^1.62.1"
}, },
"build": { "build": {
"appId": "com.masteracnolo.freedomloader", "appId": "com.masteracnolo.freedomloader",
@@ -35,7 +65,8 @@
{ {
"provider": "github", "provider": "github",
"owner": "MasterAcnolo", "owner": "MasterAcnolo",
"repo": "Freedom-Loader" "repo": "Freedom-Loader",
"releaseType": "draft"
} }
], ],
"files": [ "files": [
@@ -53,14 +84,18 @@
"from": "theme", "from": "theme",
"to": "theme" "to": "theme"
}, },
{
"from": "build/confirm-icon.png",
"to": "confirm-icon.png"
},
{ {
"from": "build/app-icon.ico", "from": "build/app-icon.ico",
"to": "app-icon.ico" "to": "app-icon.ico"
}, },
{
"from": "build/app-icon-64x64.png",
"to": "app-icon-64x64.png"
},
{
"from": "build/confirm-icon.png",
"to": "confirm-icon.png"
},
{ {
"from": "build/banner.bmp", "from": "build/banner.bmp",
"to": "banner.bmp" "to": "banner.bmp"
@@ -73,22 +108,6 @@
"from": "build/error.png", "from": "build/error.png",
"to": "error.png" "to": "error.png"
}, },
{
"from": "ressources/yt-dlp.exe",
"to": "binaries/yt-dlp.exe"
},
{
"from": "ressources/ffmpeg.exe",
"to": "binaries/ffmpeg.exe"
},
{
"from": "ressources/ffprobe.exe",
"to": "binaries/ffprobe.exe"
},
{
"from": "ressources/deno.exe",
"to": "binaries/deno.exe"
},
{ {
"from": "config/config.default.json", "from": "config/config.default.json",
"to": "config/config.default.json" "to": "config/config.default.json"
@@ -96,7 +115,26 @@
], ],
"win": { "win": {
"target": "nsis", "target": "nsis",
"icon": "build/app-icon.ico" "icon": "build/app-icon.ico",
"artifactName": "Freedom-Loader-Setup-${version}.${ext}",
"extraResources": [
{
"from": "resources/binaries/win-32/yt-dlp.exe",
"to": "binaries/yt-dlp.exe"
},
{
"from": "resources/binaries/win-32/ffmpeg.exe",
"to": "binaries/ffmpeg.exe"
},
{
"from": "resources/binaries/win-32/ffprobe.exe",
"to": "binaries/ffprobe.exe"
},
{
"from": "resources/binaries/win-32/deno.exe",
"to": "binaries/deno.exe"
}
]
}, },
"nsis": { "nsis": {
"oneClick": false, "oneClick": false,
@@ -108,6 +146,51 @@
"createDesktopShortcut": true, "createDesktopShortcut": true,
"createStartMenuShortcut": true, "createStartMenuShortcut": true,
"shortcutName": "Freedom Loader" "shortcutName": "Freedom Loader"
},
"linux": {
"target": [
"AppImage",
"deb",
"snap"
],
"category": "Utility",
"icon": "build/app-icon.png",
"executableName": "freedom-loader",
"extraResources": [
{
"from": "resources/binaries/linux/yt-dlp",
"to": "binaries/yt-dlp"
},
{
"from": "resources/binaries/linux/ffmpeg",
"to": "binaries/ffmpeg"
},
{
"from": "resources/binaries/linux/ffprobe",
"to": "binaries/ffprobe"
},
{
"from": "resources/binaries/linux/deno",
"to": "binaries/deno"
}
]
},
"deb": {
"fpm": [
"./package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml"
]
},
"rpm": {
"fpm": [
"./package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml"
]
},
"snap": {
"confinement": "strict",
"plugs": [
"default",
"removable-media"
]
} }
} }
} }

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.masteracnolo.freedomloader</id>
<name>Freedom Loader</name>
<summary>GUI for yt-dlp to download video and audio with metadata</summary>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-only</project_license>
<developer id="com.masteracnolo">
<name>MasterAcnolo</name>
</developer>
<description>
<p>
Freedom Loader is a free and open-source graphical interface built
around yt-dlp, the powerful command-line media downloader. It lets
you download video and audio from hundreds of supported websites
without ever touching a terminal, with no ads, no tracking, and
no questionable third-party services.
</p>
<p>
Under the hood, Freedom Loader bundles yt-dlp and ffmpeg directly,
so there is nothing extra to install: paste a link, choose a
format and quality, and the app handles fetching, converting
and tagging the file for you.
</p>
<p>Features include:</p>
<ul>
<li>MP4 video or MP3 audio downloads with embedded metadata and thumbnails</li>
<li>Quality selection from best to specific resolutions (1080p, 720p, 480p)</li>
<li>Codec choice: H.264, H.265, AV1, VP9 and Theora</li>
<li>Playlist downloads with automatic per-playlist folders</li>
<li>Cookie-based access to age-restricted and member-only content</li>
<li>Custom theme system with a web-based theme creator</li>
<li>Real-time download progress with network speed indicator</li>
<li>Optional Discord Rich Presence integration</li>
<li>Automatic application and download-engine updates</li>
</ul>
<p>
DRM-protected content is not supported and cannot be downloaded.
</p>
</description>
<launchable type="desktop-id">freedom-loader.desktop</launchable>
<url type="homepage">https://github.com/MasterAcnolo/Freedom-Loader</url>
<url type="bugtracker">https://github.com/MasterAcnolo/Freedom-Loader/issues</url>
<url type="vcs-browser">https://github.com/MasterAcnolo/Freedom-Loader</url>
<categories>
<category>AudioVideo</category>
<category>Utility</category>
<category>Network</category>
</categories>
<releases>
<release version="1.6.0" date="2026-06-23">
<description>
<p>Latest stable release.</p>
</description>
</release>
</releases>
<content_rating type="oars-1.1"/>
<provides>
<binary>freedom-loader</binary>
</provides>
</component>

9
package/copr-build.sh Normal file
View File

@@ -0,0 +1,9 @@
#!/bin/sh -xe
git clone --depth 1 https://github.com/MasterAcnolo/Freedom-Loader.git src
cd src
npm ci
npx electron-builder --linux dir
VERSION=$(node -p "require('./package.json').version")
tar czf "$resultdir/freedom-loader-$VERSION.tar.gz" -C dist linux-unpacked
sed "s/__VERSION__/$VERSION/" package/freedom-loader.spec > "$resultdir/freedom-loader.spec"

View File

@@ -0,0 +1,25 @@
Name: freedom-loader
Version: __VERSION__
Release: 1%{?dist}
Summary: GUI for yt-dlp media downloads
License: GPL-3.0-only
Source0: freedom-loader-__VERSION__.x86_64.rpm
BuildArch: x86_64
Requires: glibc, gtk3, nss, libXScrnSaver
%description
GUI for yt-dlp media downloads.
%install
rpm2cpio %{SOURCE0} | (cd %{buildroot} && cpio -idmv)
mkdir -p %{buildroot}/usr/bin
ln -sf "/opt/Freedom Loader/freedom-loader" %{buildroot}/usr/bin/freedom-loader
%files
/opt/*
/usr/bin/freedom-loader
/usr/share/applications/*
/usr/share/metainfo/*
/usr/share/icons/hicolor
%changelog

View File

@@ -9,6 +9,11 @@ const { contextBridge, ipcRenderer } = require("electron");
*/ */
contextBridge.exposeInMainWorld("electronAPI", { contextBridge.exposeInMainWorld("electronAPI", {
/**
* Return process.platform to renderer
*/
getProcessPlatform: () => process.platform,
/** /**
* Returns the default system download directory. * Returns the default system download directory.
*/ */

View File

@@ -106,10 +106,10 @@
</div> </div>
<div class="setting-item"> <div class="setting-item">
<input type="checkbox" data-key="autoCheckInfo" id="autoCheckInfo"> <input data-key="keepPlaylistOrder" id="keepPlaylistOrder" type="checkbox">
<div class="setting-info"> <div class="setting-info">
<span class="setting-title">Auto Fetch Info</span> <span class="setting-title">Keep Playlist Order</span>
<small>Automatically fetch information for new downloads.</small> <small>Add index before output file, to keep the order.</small>
</div> </div>
</div> </div>
</div> </div>
@@ -127,15 +127,31 @@
</div> </div>
</div> </div>
<!-- <div class="setting-item"> <div class="setting-item">
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck"> <input data-key="systemTray" id="systemTray" type="checkbox">
<div class="setting-info"> <div class="setting-info">
<span class="setting-title">Output title (special chars)</span> <span class="setting-title">System Tray</span>
<small>Check for unusual characters in output titles.</small> <small>Minimize app on close, in the System Tray.</small>
</div>
</div> </div>
</div> -->
<div class="setting-item"> <!-- <div class="setting-item">
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
<div class="setting-info">
<span class="setting-title">Output title (special chars)</span>
<small>Check for unusual characters in output titles.</small>
</div>
</div> -->
<div class="setting-item">
<input data-key="autoCheckInfo" id="autoCheckInfo" type="checkbox">
<div class="setting-info">
<span class="setting-title">Auto Fetch Info</span>
<small>Automatically fetch information for new downloads.</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="addMetadata" id="addMetadata"> <input type="checkbox" data-key="addMetadata" id="addMetadata">
<div class="setting-info"> <div class="setting-info">
<span class="setting-title">Add Metadata</span> <span class="setting-title">Add Metadata</span>
@@ -290,6 +306,7 @@
<!-- Scripts --> <!-- Scripts -->
<script src="script/toast.js"></script> <script src="script/toast.js"></script>
<script src="script/customConfiguration.js"></script>
<script src="script/clipboardPaste.js"></script> <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>

View File

@@ -1,21 +1,54 @@
/** /**
* Retrieves the application version from the Electron main process * Retrieves the application version and build state from the backend.
* and displays it in the UI version badge (bottom-right corner). * Displays a formatted string indicating packaging state and preview status.
* Hides the "Packaged" state for production builds to keep the UI clean.
* *
* This is mainly used for debugging and build identification. * Example outputs: "Not Packaged Preview v1.6.2" or "Preview v1.6.2"
*
* @returns {Promise<void>}
*/ */
async function versionLabel() { async function versionLabel() {
/**
* Raw version string from the backend (e.g., "dev-1.6.2-preview" or "v1.6.2")
* @type {string}
*/
const appVersion = await window.electronAPI.getVersion(); const appVersion = await window.electronAPI.getVersion();
/** /**
* UI element displaying the current application version. * UI element displaying the current application version.
* Updated at runtime after IPC call resolves. * @type {HTMLElement | null}
*/ */
const versionBadge = document.getElementById("version-badge"); const versionBadge = document.getElementById("version-badge");
if (!versionBadge) return; if (!versionBadge) return;
versionBadge.textContent = `${appVersion}`; /**
* Extracts the numeric semantic version (e.g., "1.6.2").
* @type {RegExpMatchArray | null}
*/
const versionMatch = appVersion.match(/(\d+\.\d+\.\d+)/);
const baseVersion = versionMatch ? versionMatch[0] : "Unknown";
/**
* Determines the packaging state. The backend prepends "dev-" if unpackaged.
* @type {boolean}
*/
const isPackaged = !appVersion.includes("dev-");
/**
* Determines if the current build is a preview version.
* @type {boolean}
*/
const isPreview = appVersion.includes("preview");
/**
* Construct the final display string.
* If packaged, we don't mention it. If not, we explicitly say "Not Packaged ".
*/
const packageStateStr = isPackaged ? "" : "Not Packaged ";
const previewStr = isPreview ? "Preview " : "";
versionBadge.textContent = `${packageStateStr}${previewStr}v${baseVersion}`;
} }
versionLabel(); versionLabel();

View File

@@ -17,9 +17,7 @@ document.addEventListener("DOMContentLoaded", () => {
*/ */
pasteBtn.addEventListener("click", async () => { pasteBtn.addEventListener("click", async () => {
try { try {
const text = await navigator.clipboard.readText(); urlInput.value = await navigator.clipboard.readText();
urlInput.value = text;
// Notify frameworks / listeners bound to input changes // Notify frameworks / listeners bound to input changes
urlInput.dispatchEvent(new Event("input", { bubbles: true })); urlInput.dispatchEvent(new Event("input", { bubbles: true }));

View File

@@ -0,0 +1,27 @@
async function initPlatformRestrictions() {
const platform = await window.electronAPI.getProcessPlatform();
const themeSelect = document.getElementById("themeSelect");
const refreshBtn = document.getElementById("refresh-themes-btn");
/**
* Is the OS windows
* @type {boolean}
*/
const isWindows = platform === "win32";
console.log(isWindows);
// If it's windows, the button is not disabled.
themeSelect.disabled = !isWindows;
refreshBtn.disabled = !isWindows;
themeSelect.classList.toggle("disabled", !isWindows);
refreshBtn.classList.toggle("disabled", !isWindows);
if (!isWindows) {
themeSelect.title = "Themes are only available on Windows";
refreshBtn.title = "Themes are only available on Windows";
}
}

View File

@@ -32,52 +32,52 @@ function applyTheme(theme) {
} }
// CSS variables (design tokens) // CSS variables (design tokens)
root.style.setProperty("--background-color", style.colors?.background || ""); root.style.setProperty("--background-color", style.colors?.background || "#121212");
root.style.setProperty("--default-text-color", style.colors?.text?.default || ""); root.style.setProperty("--default-text-color", style.colors?.text?.default || "#e0e0e0");
root.style.setProperty("--subtitle-color", style.colors?.text?.subtitle || ""); root.style.setProperty("--subtitle-color", style.colors?.text?.subtitle || "#2196f3");
root.style.setProperty("--audio-only-label-color", style.colors?.text?.audioOnly || ""); root.style.setProperty("--audio-only-label-color", style.colors?.text?.audioOnly || "#f5f5f5");
root.style.setProperty("--form-bg-color", style.form?.background || ""); root.style.setProperty("--form-bg-color", style.form?.background || "#1e1e1e");
root.style.setProperty("--form-input-bg-color", style.form?.input?.background || ""); root.style.setProperty("--form-input-bg-color", style.form?.input?.background || "#2a2a2a");
root.style.setProperty("--form-input-border-color", style.form?.input?.border || ""); root.style.setProperty("--form-input-border-color", style.form?.input?.border || "#444444");
root.style.setProperty("--form-input-border-focus-color", style.form?.input?.borderFocus || ""); root.style.setProperty("--form-input-border-focus-color", style.form?.input?.borderFocus || "#2196f3");
root.style.setProperty("--form-input-text-color", style.form?.input?.text || ""); root.style.setProperty("--form-input-text-color", style.form?.input?.text || "#f5f5f5");
root.style.setProperty("--form-input-placeholder-color", style.form?.input?.placeholder || ""); root.style.setProperty("--form-input-placeholder-color", style.form?.input?.placeholder || "#888888");
root.style.setProperty("--form-button-bg-color", style.form?.button?.background || ""); root.style.setProperty("--form-button-bg-color", style.form?.button?.background || "#2196f3");
root.style.setProperty("--form-button-text-color", style.form?.button?.text || ""); root.style.setProperty("--form-button-text-color", style.form?.button?.text || "#ffffff");
root.style.setProperty("--form-button-bg-hover-color", style.form?.button?.hover || ""); root.style.setProperty("--form-button-bg-hover-color", style.form?.button?.hover || "#1976d2");
root.style.setProperty("--paste-button-icon-color", style.form?.pasteButtonIcon || ""); root.style.setProperty("--paste-button-icon-color", style.form?.pasteButtonIcon || "#ffffff");
root.style.setProperty("--checkbox-unchecked-bg-color", style.checkbox?.background?.unchecked || ""); root.style.setProperty("--checkbox-unchecked-bg-color", style.checkbox?.background?.unchecked || "#555555");
root.style.setProperty("--checkbox-checked-bg-color", style.checkbox?.background?.checked || ""); root.style.setProperty("--checkbox-checked-bg-color", style.checkbox?.background?.checked || "#2196f3");
root.style.setProperty("--checkbox-checkmark-border-color", style.checkbox?.checkmarkBorder || ""); root.style.setProperty("--checkbox-checkmark-border-color", style.checkbox?.checkmarkBorder || "#ffffff");
root.style.setProperty("--checkbox-pulse-color", style.checkbox?.pulse || ""); root.style.setProperty("--checkbox-pulse-color", style.checkbox?.pulse || "rgba(33, 150, 243, 0.5)");
root.style.setProperty("--download-status-color", style.download?.status || ""); root.style.setProperty("--download-status-color", style.download?.status || "#2196f3");
root.style.setProperty("--progress-bar-bg-color", style.progressBar?.background || ""); root.style.setProperty("--progress-bar-bg-color", style.progressBar?.background || "#444444");
root.style.setProperty("--progress-bar-fill-color", style.progressBar?.fill || ""); root.style.setProperty("--progress-bar-fill-color", style.progressBar?.fill || "#2196f3");
root.style.setProperty("--video-info-bg-color", style.videoInfo?.background || ""); root.style.setProperty("--video-info-bg-color", style.videoInfo?.background || "#1a1a1a");
root.style.setProperty("--video-info-text-color", style.videoInfo?.text || ""); root.style.setProperty("--video-info-text-color", style.videoInfo?.text || "#e0e0e0");
root.style.setProperty("--video-info-heading-color", style.videoInfo?.heading || ""); root.style.setProperty("--video-info-heading-color", style.videoInfo?.heading || "#2196f3");
root.style.setProperty("--video-info-list-color", style.videoInfo?.list || ""); root.style.setProperty("--video-info-list-color", style.videoInfo?.list || "#cccccc");
root.style.setProperty("--video-info-list-strong-color", style.videoInfo?.strong || ""); root.style.setProperty("--video-info-list-strong-color", style.videoInfo?.strong || "#ffffff");
root.style.setProperty("--video-info-link-color", style.videoInfo?.link || ""); root.style.setProperty("--video-info-link-color", style.videoInfo?.link || "#2196f3");
root.style.setProperty("--video-info-link-hover-color", style.videoInfo?.linkHover || ""); root.style.setProperty("--video-info-link-hover-color", style.videoInfo?.linkHover || "#1976d2");
root.style.setProperty("--playlist-background-color", style.playlist?.background || ""); root.style.setProperty("--playlist-background-color", style.playlist?.background || "#303030");
root.style.setProperty("--settings-button-bg-color", style.settings?.button?.background || ""); root.style.setProperty("--settings-button-bg-color", style.settings?.button?.background || "#1e1e1e");
root.style.setProperty("--settings-button-text-color", style.settings?.button?.text || ""); root.style.setProperty("--settings-button-text-color", style.settings?.button?.text || "#e0e0e0");
root.style.setProperty("--open-theme-button-bg-color", style.settings?.openThemeButton?.background || ""); root.style.setProperty("--open-theme-button-bg-color", style.settings?.openThemeButton?.background || "#2196f3");
root.style.setProperty("--open-theme-button-text-color", style.settings?.openThemeButton?.text || ""); root.style.setProperty("--open-theme-button-text-color", style.settings?.openThemeButton?.text || "#ffffff");
root.style.setProperty("--open-json-button-bg-color", style.settings?.openJsonButton?.background || style.settings?.button?.background || ""); root.style.setProperty("--open-json-button-bg-color", style.settings?.openJsonButton?.background || style.settings?.button?.background || "#1e1e1e");
root.style.setProperty("--open-json-button-text-color", style.settings?.openJsonButton?.text || style.settings?.button?.text || ""); root.style.setProperty("--open-json-button-text-color", style.settings?.openJsonButton?.text || style.settings?.button?.text || "#e0e0e0");
root.style.setProperty("--settings-modal-bg-color", style.settings?.background?.modal || ""); root.style.setProperty("--settings-modal-bg-color", style.settings?.background?.modal || "#1e1e1e");
root.style.setProperty("--settings-section-bg-color", style.settings?.background?.section || ""); root.style.setProperty("--settings-section-bg-color", style.settings?.background?.section || "#2a2a2a");
root.style.setProperty("--settings-text-color", style.settings?.text || ""); root.style.setProperty("--settings-text-color", style.settings?.text || "#e0e0e0");
root.style.setProperty("--settings-subtitle-color", style.settings?.subtitle || ""); root.style.setProperty("--settings-subtitle-color", style.settings?.subtitle || "#aaaaaa");
const subtitleEl = document.getElementById("subtitle"); const subtitleEl = document.getElementById("subtitle");
if (subtitleEl) subtitleEl.textContent = theme.subtitle || theme.name; if (subtitleEl) subtitleEl.textContent = theme.subtitle || theme.name;
@@ -116,7 +116,102 @@ function populateThemeSelect(themes) {
*/ */
async function initThemes() { async function initThemes() {
loadedThemes = await window.electronAPI.getThemes(); loadedThemes = await window.electronAPI.getThemes();
if (!loadedThemes.length) return;
if (!loadedThemes.length) {
const defaultTheme = {
id: "dark",
name: "Dark",
subtitle: "Darkness is my ally",
style: {
background: {
size: "cover",
position: "center",
attachment: "fixed"
},
colors: {
background: "#121212",
text: {
default: "#e0e0e0",
subtitle: "#2196f3",
audioOnly: "#f5f5f5"
}
},
form: {
background: "#1e1e1e",
input: {
background: "#2a2a2a",
border: "#444444",
borderFocus: "#2196f3",
text: "#f5f5f5",
placeholder: "#888888"
},
button: {
background: "#2196f3",
text: "#ffffff",
hover: "#1976d2"
},
pasteButtonIcon: "#ffffff"
},
checkbox: {
background: {
unchecked: "#555555",
checked: "#2196f3"
},
checkmarkBorder: "#ffffff",
pulse: "rgba(33, 150, 243, 0.5)"
},
download: {
status: "#2196f3"
},
progressBar: {
background: "#444444",
fill: "#2196f3"
},
videoInfo: {
background: "#1a1a1a",
text: "#e0e0e0",
heading: "#2196f3",
list: "#cccccc",
strong: "#ffffff",
link: "#2196f3",
linkHover: "#1976d2"
},
playlist: {
background: "#303030"
},
settings: {
button: {
background: "#1e1e1e",
text: "#e0e0e0"
},
openThemeButton: {
background: "#2196f3",
text: "#ffffff"
},
openJsonButton: {
background: "#2196f3",
text: "#ffffff"
},
background: {
modal: "#1e1e1e",
section: "#2a2a2a"
},
text: "#e0e0e0",
subtitle: "#aaaaaa"
}
}
};
loadedThemes = [defaultTheme];
}
populateThemeSelect(loadedThemes); populateThemeSelect(loadedThemes);

View File

@@ -30,15 +30,15 @@ function formatSize(bytes) {
*/ */
async function fetchVideoInfo(url) { async function fetchVideoInfo(url) {
try { try {
const res = await fetch("/info", { const res = await fetch(`/info?url=${encodeURIComponent(url)}`, {
method: "POST", method: "GET",
headers: { "Content-Type": "application/x-www-form-urlencoded" }, headers: {"Accept": "application/json"}
body: new URLSearchParams({ url }),
}); });
if (!res.ok) return { error: `An Error occured when fetching info` };
const data = await res.json(); const data = await res.json();
if (!res.ok) return {error: `An Error occured when fetching info`};
if (!data) return { error: "Data is Missing" }; if (!data) return { error: "Data is Missing" };
return data; return data;
@@ -271,6 +271,6 @@ async function init() {
}) })
}; }
init(); init();

View File

@@ -16,6 +16,14 @@
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
/* Mark element as disabled */
.disabled {
pointer-events: none;
opacity: 0.5;
filter: grayscale(40%);
cursor: not-allowed;
}
/* Because why not */ /* Because why not */
#reference{ #reference{
position: fixed; position: fixed;

BIN
resources/binaries/linux/yt-dlp Executable file

Binary file not shown.

116
scripts/create-copr-package.sh Executable file
View File

@@ -0,0 +1,116 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
VERSION=$(node -p "require('$ROOT_DIR/package.json').version")
RPM_OUT="$ROOT_DIR/dist/freedom-loader-${VERSION}.x86_64.rpm"
# ------------------------------------------------------------
# Resolve fpm from electron-builder cache
# Excludes Ruby gem wrappers, targets standalone binaries only
# Prefers fpm@2.x over legacy fpm-1.9.x
# ------------------------------------------------------------
FPM=$(find ~/.cache/electron-builder -name "fpm" -type f 2>/dev/null \
| grep -v "/gems/bin/fpm" \
| grep -v "/gems/gems/" \
| grep -v "/lib/app/bin/fpm" \
| grep "fpm@" \
| head -1)
# Fallback to legacy fpm-1.9.x if fpm@2.x not found
if [ -z "$FPM" ]; then
FPM=$(find ~/.cache/electron-builder -name "fpm" -type f 2>/dev/null \
| grep -v "/gems/" \
| grep -v "/lib/app/bin/fpm" \
| head -1)
fi
if [ -z "$FPM" ]; then
echo "Error: fpm not found in electron-builder cache. Run 'npm run build:linux' once first to download it."
exit 1
fi
echo "Using fpm: $FPM"
# Check that linux-unpacked exists
if [ ! -d "$ROOT_DIR/dist/linux-unpacked" ]; then
echo "Error: dist/linux-unpacked not found. Run 'npm run build:linux' first."
exit 1
fi
# ------------------------------------------------------------
# Prepare staging files
# fpm requires exact file-to-destination mappings to avoid
# scanning system directories (which causes permission errors)
# ------------------------------------------------------------
# Shell wrapper for /usr/bin/freedom-loader
TMP_WRAPPER="$ROOT_DIR/.rpm-pkg-tmp-wrapper"
mkdir -p "$TMP_WRAPPER"
cat > "$TMP_WRAPPER/freedom-loader" << 'EOF'
#!/bin/sh
exec /opt/freedom-loader/freedom-loader "$@"
EOF
chmod +x "$TMP_WRAPPER/freedom-loader"
# Desktop entry file
TMP_DESKTOP="$ROOT_DIR/.rpm-pkg-tmp-desktop"
mkdir -p "$TMP_DESKTOP"
cat > "$TMP_DESKTOP/freedom-loader.desktop" << 'EOF'
[Desktop Entry]
Name=Freedom Loader
Exec=/opt/freedom-loader/freedom-loader %U
Icon=freedom-loader
Type=Application
Categories=AudioVideo;Utility;Network;
Comment=Free and open-source GUI for yt-dlp
StartupWMClass=Freedom Loader
EOF
# ------------------------------------------------------------
# Step 1 - Build .rpm from linux-unpacked via fpm
# ------------------------------------------------------------
echo "Building RPM..."
"$FPM" -s dir -t rpm --force \
--rpm-use-file-permissions \
--name freedom-loader \
--version "$VERSION" \
--architecture x86_64 \
--description "Free and open-source GUI for yt-dlp" \
--url "https://masteracnolo.github.io/Freedom-Loader-Site/" \
--maintainer "MasterAcnolo <MasterAcnolo@users.noreply.github.com>" \
--license "GPL-3.0-only" \
--depends gtk3 \
--depends nss \
--depends libXScrnSaver \
--depends at-spi2-core \
--rpm-attr "4755,root,root:/opt/freedom-loader/chrome-sandbox" \
--package "$RPM_OUT" \
"$ROOT_DIR/dist/linux-unpacked/=/opt/freedom-loader" \
"$TMP_WRAPPER/freedom-loader=/usr/bin/freedom-loader" \
"$TMP_DESKTOP/freedom-loader.desktop=/usr/share/applications/freedom-loader.desktop" \
"$ROOT_DIR/build/app-icon.png=/usr/share/icons/hicolor/512x512/apps/freedom-loader.png" \
"$ROOT_DIR/package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml"
rm -rf "$TMP_WRAPPER" "$TMP_DESKTOP"
echo "RPM built: $RPM_OUT"
# ------------------------------------------------------------
# Step 2 - Build SRPM for COPR
# Wraps the .rpm above into a source RPM for COPR submission
# ------------------------------------------------------------
echo "Building SRPM for COPR..."
mkdir -p "$ROOT_DIR/srpm-out"
TMP_DIR="$ROOT_DIR/.copr-build-tmp"
mkdir -p "$TMP_DIR"
TMP_SPEC="$TMP_DIR/freedom-loader.spec"
sed "s/__VERSION__/$VERSION/g" "$ROOT_DIR/package/freedom-loader.spec" > "$TMP_SPEC"
rpmbuild -bs "$TMP_SPEC" \
--define "_sourcedir $ROOT_DIR/dist" \
--define "_srcrpmdir $ROOT_DIR/srpm-out"
rm -rf "$TMP_DIR"
echo "SRPM built: $ROOT_DIR/srpm-out/freedom-loader-$VERSION-1*.src.rpm"

187
scripts/release.sh Executable file
View File

@@ -0,0 +1,187 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
VERSION=$(node -p "require('$ROOT_DIR/package.json').version")
TAG="$VERSION"
# Parse arguments
PUBLISH_SNAP=false
PUBLISH_COPR=false
DRY_RUN=false
for arg in "$@"; do
case $arg in
--snap) PUBLISH_SNAP=true ;;
--copr) PUBLISH_COPR=true ;;
--publish) PUBLISH_SNAP=true; PUBLISH_COPR=true ;;
--dry-run) DRY_RUN=true ;;
*) echo "⚠️ Argument inconnu : $arg" ; exit 1 ;;
esac
done
echo ""
echo "============================================"
echo " Freedom Loader - Release Pipeline"
echo " Version : $VERSION"
echo " Tag : $TAG"
echo " Snap : $PUBLISH_SNAP"
echo " COPR : $PUBLISH_COPR"
echo " Dry run : $DRY_RUN"
echo "============================================"
echo ""
# ------------------------------------------------------------
# Step 0.1 - Check Dependencies
# ------------------------------------------------------------
echo "[0/5] Checking dependencies..."
command -v gh >/dev/null 2>&1 || { echo "'gh' CLI missing, need to be installed (https://cli.github.com/)."; exit 1; }
if [ "$PUBLISH_SNAP" = true ] && [ "$DRY_RUN" = false ]; then
command -v snapcraft >/dev/null 2>&1 || { echo "'snapcraft' missing, need to be installed."; exit 1; }
fi
if [ "$PUBLISH_COPR" = true ] && [ "$DRY_RUN" = false ]; then
command -v copr-cli >/dev/null 2>&1 || { echo "'copr-cli' missing, need to be installed."; exit 1; }
fi
echo "All dependencies found"
# ------------------------------------------------------------
# Step 0.2 - Clean Workspace
# ------------------------------------------------------------
echo "[0/5] Cleaning workspace..."
# On supprime les dossiers générés lors des builds précédents
rm -rf "$ROOT_DIR/dist" "$ROOT_DIR/srpm-out"
echo "Workspace cleaned (dist/ and srpm-out/ removed)"
# ------------------------------------------------------------
# Step 1 - Build Linux packages (AppImage, deb, snap)
# ------------------------------------------------------------
echo "[1/5] Building Linux packages..."
cd "$ROOT_DIR"
npm run build:linux
echo "Linux packages built"
# ------------------------------------------------------------
# Step 2 - Build RPM binary + SRPM for COPR
# Uses the create-copr-package.sh script which handles:
# - fpm binary resolution from electron-builder cache
# - .rpm generation from linux-unpacked
# - .src.rpm generation for COPR submission
# ------------------------------------------------------------
echo "[2/5] Building RPM and SRPM..."
bash "$ROOT_DIR/scripts/create-copr-package.sh"
echo "RPM and SRPM built"
# ------------------------------------------------------------
# Step 3 - Build Windows installer
# Note: requires Windows binaries to be present locally under
# resources/binaries/win-32/ (not committed to the repo)
# ------------------------------------------------------------
echo "[3/5] Building Windows installer..."
npm run build:win
echo " Windows installer built"
# ------------------------------------------------------------
# Step 4 - Create a draft release on GitHub
# Release notes are pre-filled with the standard template.
# Edit and publish manually from the GitHub web interface.
# Requires gh CLI to be installed and authenticated.
# ------------------------------------------------------------
echo "[4/5] Creating GitHub draft release $TAG..."
# Utilise une méthode un peu plus robuste pour trouver l'ancien tag (ignore le HEAD actuel s'il est déjà tagué)
PREV_TAG=$(git rev-list --tags --skip=1 --max-count=1 | xargs git describe --tags 2>/dev/null || echo "previous")
RELEASE_NOTES="# Freedom Loader - $VERSION
## Found a bug or issue?
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}"
if [ "$DRY_RUN" = false ]; then
gh release create "$TAG" \
--title "$TAG" \
--notes "$RELEASE_NOTES" \
--draft \
"$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe" \
"$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe.blockmap" \
"$ROOT_DIR/dist/freedom-loader-${VERSION}.x86_64.rpm" \
"$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.deb" \
"$ROOT_DIR/dist/Freedom Loader-${VERSION}.AppImage" \
"$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.snap" \
"$ROOT_DIR/dist/latest.yml" \
"$ROOT_DIR/dist/latest-linux.yml"
echo "Draft release created: https://github.com/MasterAcnolo/Freedom-Loader/releases"
else
echo " [DRY RUN] gh release create $TAG (skipped)"
fi
# ------------------------------------------------------------
# Step 5 - Publish to package stores
# Snap and COPR are opt-in via --snap, --copr, or --publish.
# Without these flags, this step is skipped entirely.
# Run after the GitHub release is published (not the draft),
# so store users always get the same version as GitHub users.
# ------------------------------------------------------------
echo "[5/5] Publishing to package stores..."
if [ "$PUBLISH_SNAP" = true ]; then
echo " Publishing to Snap Store..."
if [ "$DRY_RUN" = false ]; then
export PATH=$PATH:/var/lib/snapd/snap/bin
snapcraft upload \
"$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.snap" \
--release=stable
echo " Snap published"
else
echo " [DRY RUN] snapcraft upload (skipped)"
fi
else
echo " Snap: skipped (use --snap or --publish to enable)"
fi
if [ "$PUBLISH_COPR" = true ]; then
echo " Publishing to COPR..."
if [ "$DRY_RUN" = false ]; then
copr-cli build freedom-loader \
"$ROOT_DIR/srpm-out/freedom-loader-${VERSION}-1"*.src.rpm
echo "COPR build triggered"
else
echo " [DRY RUN] copr-cli build (skipped)"
fi
else
echo " COPR: skipped (use --copr or --publish to enable)"
fi
# ------------------------------------------------------------
# Done - Summary
# ------------------------------------------------------------
echo ""
echo "============================================"
echo " Release pipeline complete."
echo ""
echo " Next steps:"
echo " 1. Fill in the release notes on GitHub"
echo " 2. Publish the draft release"
if [ "$PUBLISH_SNAP" = false ] || [ "$PUBLISH_COPR" = false ]; then
echo ""
echo " Store publishing (not done yet):"
[ "$PUBLISH_SNAP" = false ] && echo " - Snap : bash scripts/release.sh --snap"
[ "$PUBLISH_COPR" = false ] && echo " - COPR : bash scripts/release.sh --copr"
fi
echo "============================================"

View File

@@ -18,7 +18,9 @@ const { isValidUrl } = require("../helpers/validation.helpers");
*/ */
async function infoController(req, res) { async function infoController(req, res) {
const url = req.body.url || req.query.url; // Supports POST and GET requests // Previously, i support both POST and GET element, but it's kinda weird. So now it only support GET.
const url = req.query.url;
const encodedUrl = encodeURIComponent(url)
/** /**
* Basic validation: * Basic validation:
@@ -30,9 +32,10 @@ async function infoController(req, res) {
return res.status(400).send("Invalid URL Or Missing"); return res.status(400).send("Invalid URL Or Missing");
} }
logger.info(`Info request received. URL: ${url}`); logger.info(`Info request received. RAW URL: ${url}`);
logger.info(`Info request received. ENCODED: ${encodedUrl}`);
// Lightweight heuristic to detect playlist URLs // Lightweight heuristic to detect playlist URLs. It works for Youtube.
const isPlaylistUrl = url.includes("&list") || url.includes("?list"); const isPlaylistUrl = url.includes("&list") || url.includes("?list");
logger.info( logger.info(
@@ -86,6 +89,16 @@ async function infoController(req, res) {
*/ */
if (data._type === "playlist") { if (data._type === "playlist") {
// If the playlist got only one video, show it as a video.
if (data.entries && data.entries.length === 1) {
const singleVideo = parseVideo(data.entries[0]);
logger.info(`Playlist with 1 item converted to video: ${singleVideo.title}`);
return res.json(singleVideo);
}
const playlist = parsePlaylist(data); const playlist = parsePlaylist(data);
logger.info( logger.info(

View File

@@ -4,6 +4,7 @@ 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");
const config = require("../../config")
/** /**
* Validates whether the provided codec is supported by the application. * Validates whether the provided codec is supported by the application.
@@ -62,7 +63,7 @@ function validateCodec(codec){
* *
* @returns {string[]} Array of arguments ready to be passed to yt-dlp. * @returns {string[]} Array of arguments ready to be passed to yt-dlp.
*/ */
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) { function buildYtDlpArgs({url, audioOnly, quality, outputFolder, isPlaylist}) {
logger.info("--- CONFIGURATION ---"); logger.info("--- CONFIGURATION ---");
@@ -70,6 +71,7 @@ function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`); logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`); logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`); logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
logger.info(`CONFIG keepPlaylistOrder: ${configFeatures.keepPlaylistOrder}`);
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`); logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);
logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`); logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`);
logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`); logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`);
@@ -117,7 +119,27 @@ function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
}; };
args.push("-f", qualityMap[quality] || "best"); args.push("-f", qualityMap[quality] || "best");
args.push("-o", path.join(outputFolder, "%(title)s.%(ext)s"));
/**
* Default title output template
* @type {string}
*/
let outputTemplate = "%(title)s.%(ext)s";
// If it's a playlist, and we want to keep the playlist order. Add an index before the title.
if (isPlaylist && configFeatures.keepPlaylistOrder && configFeatures.autoDownloadPlaylist) {
outputTemplate = "%(playlist_index)02d - %(title)s.%(ext)s";
}
// Add a custom signature in each file downloaded with the app. It's a small things but i found it cool :)
if (configFeatures.addMetadata) {
args.push(
"--postprocessor-args",
`ffmpeg:-metadata comment="Downloaded with Freedom Loader v${config.version}"`
);
}
args.push("-o", path.join(outputFolder, outputTemplate));
args.push(url); args.push(url);
return args.filter(Boolean); return args.filter(Boolean);

View File

@@ -22,7 +22,8 @@ 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 userProfile = os.homedir();
const firefoxPath = getFirefoxProfilePath();
/** /**
* Browsers supported by yt-dlp cookie extraction. * Browsers supported by yt-dlp cookie extraction.
@@ -32,7 +33,7 @@ function getUserBrowser() {
* once compatibility has been verified. * once compatibility has been verified.
*/ */
const browsers = [ const browsers = [
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") }, { name: "firefox", path: firefoxPath },
// { 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") },
// { name: "brave", path: path.join(userProfile, "AppData", "Local", "BraveSoftware", "Brave-Browser", "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: "edge", path: path.join(userProfile, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default") },
@@ -44,7 +45,7 @@ function getUserBrowser() {
// Search for the first available browser profile. // Search for the first available browser profile.
for (const browser of browsers) { for (const browser of browsers) {
if (fs.existsSync(browser.path)) { if (browser.path && fs.existsSync(browser.path)) {
logger.info(`Browser found: ${browser.name}`); logger.info(`Browser found: ${browser.name}`);
return browser.name; return browser.name;
} }
@@ -52,11 +53,50 @@ function getUserBrowser() {
// No supported browser found => Notify User // No supported browser found => Notify User
logger.warn("No supported browser found on the system"); logger.warn("No supported browser found on the system");
notify.notifyFirefoxBrowserMissing();
// If you somehow managed to live without Firefox and need help installing it. - Don't applied to my Linux chad
if (process.platform === "win32") {
notify.notifyFirefoxBrowserMissing();
}
// Fallback to Firefox and let yt-dlp handle the error gracefully. // Fallback to Firefox and let yt-dlp handle the error gracefully.
// This prevents the application from crashing // This prevents the application from crashing
return "firefox"; return "firefox";
} }
function getFirefoxProfilePath() {
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 module.exports = getUserBrowser

View File

@@ -2,6 +2,12 @@ const { Notification, shell } = require("electron");
const { iconPaths } = require("./path.helpers"); const { iconPaths } = require("./path.helpers");
const { logger } = require("../logger"); const { logger } = require("../logger");
/**
* Set that is holding all notifications. Used to avoid Race Condition and notification crash
* @type {Set<any>}
*/
const activeNotifications = new Set();
/** /**
* Displays a system notification when a download completes successfully. * Displays a system notification when a download completes successfully.
* *
@@ -21,7 +27,27 @@ function notifyDownloadFinished(folder, notifyEnabled = true) {
icon: iconPaths.confirm, icon: iconPaths.confirm,
}); });
notif.on("click", () => shell.openPath(folder)); // Protect notification to garbage collection and race condition, by adding a 150ms timeout before trying to open the folder
activeNotifications.add(notif);
notif.on("click", () => {
activeNotifications.delete(notif);
setTimeout(() => {
shell.openPath(folder).then((errorMessage) => {
if (errorMessage) {
logger.error(`Impossible d'ouvrir le dossier : ${errorMessage}`);
}
}).catch(err => {
logger.error(`Erreur inattendue de shell.openPath : ${err}`);
});
}, 150);
});
notif.on("close", () => {
activeNotifications.delete(notif);
});
notif.show(); notif.show();
} }
@@ -41,11 +67,16 @@ function notifyCookiesBrowserError(){
icon: iconPaths.error, icon: iconPaths.error,
}); });
notif.on("click", () => activeNotifications.add(notif);
shell.openExternal(
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1" notif.on("click", () => {
) shell.openExternal("https://www.firefox.com/en-US/download/");
); activeNotifications.delete(notif);
});
notif.on("close", () => {
activeNotifications.delete(notif);
});
notif.show(); notif.show();
} }
@@ -65,11 +96,16 @@ function notifyFirefoxBrowserMissing() {
icon: iconPaths.error, icon: iconPaths.error,
}); });
notif.on("click", () => activeNotifications.add(notif);
shell.openExternal(
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1" notif.on("click", () => {
) shell.openExternal("https://www.firefox.com/en-US/download/");
); activeNotifications.delete(notif);
});
notif.on("close", () => {
activeNotifications.delete(notif);
});
notif.show(); notif.show();
} }

View File

@@ -1,18 +1,47 @@
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.js"); const config = require("../../config.js");
const { logger } = require("../logger.js"); /**
* Is the OS windows
*/
const isWindows = process.platform === 'win32'
// Centralized resource paths /**
* On windows the binaries are name.exe on Linux/macOS no
*/
const ext = isWindows ? '.exe' : ''
/** Centralized resource paths */
const resourcesPath = config.devMode const resourcesPath = config.devMode
? path.join(__dirname, "../../ressources") ? path.join(__dirname, "../../resources")
: process.resourcesPath; : process.resourcesPath;
// Default download folder (centralized) /** Default download folder (centralized) */
const defaultDownloadFolder = path.join(os.homedir(), "Downloads", "Freedom Loader"); const defaultDownloadFolder = path.join(app.getPath("downloads"), "Freedom Loader");
/**
* Current platform:
* win32 | linux | darwin
*/
const platform = process.platform;
/**
* Dev source tree uses platform-named subfolders (matches resources/binaries/{win-32,linux,darwin}).
* Packaged builds flatten everything directly under binaries/, since each
* platform build only ships its own binaries — no subfolder needed.
*/
const devPlatformFolder = {
win32: "win-32",
linux: "linux",
darwin: "darwin"
}[platform];
const binariesPath = config.devMode
? path.join(resourcesPath, "binaries", devPlatformFolder)
: path.join(resourcesPath, "binaries");
/** /**
* Runtime-resolved binary paths. * Runtime-resolved binary paths.
@@ -24,7 +53,10 @@ let ffmpegPath;
let denoPath; let denoPath;
/** Source binary bundled inside application resources (used for first-time copy) */ /** Source binary bundled inside application resources (used for first-time copy) */
const sourceYtDlp = path.join(resourcesPath, "binaries","yt-dlp.exe"); const sourceYtDlp = path.join(
binariesPath,
`yt-dlp${ext}`
);
/** /**
* Resolve binary locations depending on environment: * Resolve binary locations depending on environment:
@@ -32,16 +64,47 @@ const sourceYtDlp = path.join(resourcesPath, "binaries","yt-dlp.exe");
* - prod: extracted userData + packaged resources * - prod: extracted userData + packaged resources
*/ */
if (config.devMode) { if (config.devMode) {
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe"); userYtDlp = path.join(
ffmpegPath = path.join(__dirname, "../../ressources/"); // <- has ffmpeg.exe and ffprobe.exe binariesPath,
denoPath = path.join(__dirname, "../../ressources/deno.exe"); `yt-dlp${ext}`
} else { );
userYtDlp = path.join(app.getPath("userData"),"yt-dlp.exe");
ffmpegPath = path.join(resourcesPath, "binaries","ffmpeg.exe");
denoPath = path.join(resourcesPath, "binaries","deno.exe");
if (!fs.existsSync(userYtDlp)) { ffmpegPath = path.join(
binariesPath,
`ffmpeg${ext}`
);
denoPath = path.join(
binariesPath,
`deno${ext}`
);
} else {
userYtDlp = path.join(
app.getPath("userData"),
`yt-dlp${ext}`
);
ffmpegPath = path.join(
binariesPath,
`ffmpeg${ext}`
);
denoPath = path.join(
binariesPath,
`deno${ext}`
);
if (!fs.existsSync(userYtDlp) && fs.existsSync(sourceYtDlp)) {
fs.copyFileSync(sourceYtDlp, userYtDlp); fs.copyFileSync(sourceYtDlp, userYtDlp);
// Linux/macOS executable permission
if (!isWindows) {
try {
fs.chmodSync(userYtDlp, 0o755);
} catch (err) {
getLogger().warn(`Failed to chmod yt-dlp: ${err.message}`);
}
}
} }
} }
@@ -60,10 +123,25 @@ const iconPaths = {
* Used mainly for verification and diagnostics. * Used mainly for verification and diagnostics.
*/ */
const binaryPaths = { const binaryPaths = {
ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"), ytDlp: path.join(
ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"), binariesPath,
ffprobe: path.join(resourcesPath, "binaries", "ffprobe.exe"), `yt-dlp${ext}`
deno: path.join(resourcesPath, "binaries", "deno.exe") ),
ffmpeg: path.join(
binariesPath,
`ffmpeg${ext}`
),
ffprobe: path.join(
binariesPath,
`ffprobe${ext}`
),
deno: path.join(
binariesPath,
`deno${ext}`
)
}; };
/** /**
@@ -85,42 +163,48 @@ function initUserThemes() {
try { try {
if (!fs.existsSync(userThemesPath)) { if (!fs.existsSync(userThemesPath)) {
fs.mkdirSync(userThemesPath, { recursive: true }); fs.mkdirSync(userThemesPath, { recursive: true });
// Ensure folder is writable
fs.chmodSync(userThemesPath, 0o777); fs.chmodSync(userThemesPath, 0o777);
logger.info("Created user themes directory"); getLogger().info("Created user themes directory");
} }
// Copy default themes from resources to user directory if they don't exist
if (fs.existsSync(defaultThemesSourcePath)) { if (fs.existsSync(defaultThemesSourcePath)) {
const defaultThemes = fs.readdirSync(defaultThemesSourcePath); const defaultThemes = fs.readdirSync(defaultThemesSourcePath);
for (const theme of defaultThemes) { for (const theme of defaultThemes) {
const srcPath = path.join(defaultThemesSourcePath, theme); const srcPath = path.join(defaultThemesSourcePath, theme);
const destPath = path.join(userThemesPath, theme); const destPath = path.join(userThemesPath, theme);
const srcIsFile = !fs.statSync(srcPath).isDirectory();
if (!fs.existsSync(destPath)) { if (fs.existsSync(destPath)) {
try { const destIsDir = fs.statSync(destPath).isDirectory();
if (srcIsFile && destIsDir) {
getLogger().warn(`Corrupted theme entry: ${theme}, fixing...`);
fs.rmSync(destPath, { recursive: true, force: true });
} else {
continue;
}
}
try {
if (srcIsFile) {
fs.copyFileSync(srcPath, destPath);
fs.chmodSync(destPath, 0o666);
} else {
copyDirectory(srcPath, destPath); copyDirectory(srcPath, destPath);
fs.chmodSync(destPath, 0o777); fs.chmodSync(destPath, 0o777);
logger.info(`Copied default theme: ${theme}`);
} catch (copyErr) {
logger.warn(`Failed to copy theme ${theme}: ${copyErr.message}. Theme folder will be created, add theme files manually.`);
try {
fs.mkdirSync(destPath, { recursive: true });
fs.chmodSync(destPath, 0o777);
} catch (mkdirErr) {
logger.warn(`Could not create theme directory ${theme}: ${mkdirErr.message}`);
}
} }
getLogger().info(`Copied default theme: ${theme}`);
} catch (copyErr) {
getLogger().warn(`Failed to copy theme ${theme}: ${copyErr.message}`);
} }
} }
} else { } else {
logger.warn(`Default themes source path not found: ${defaultThemesSourcePath}`); getLogger().warn(`Default themes source path not found: ${defaultThemesSourcePath}`);
} }
} catch (err) { } catch (err) {
logger.error(`Failed to initialize user themes: ${err.message}`); getLogger().error(`Failed to initialize user themes: ${err.message}`);
} }
} }
/** /**
* Recursively copies a directory and its contents. * Recursively copies a directory and its contents.
* *
@@ -131,10 +215,14 @@ function initUserThemes() {
function copyDirectory(src, dest) { function copyDirectory(src, dest) {
try { try {
if (!fs.existsSync(dest)) { if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true, mode: 0o777 }); fs.mkdirSync(dest, {
recursive: true,
mode: 0o777
});
} }
const files = fs.readdirSync(src); const files = fs.readdirSync(src);
for (const file of files) { for (const file of files) {
const srcFile = path.join(src, file); const srcFile = path.join(src, file);
const destFile = path.join(dest, file); const destFile = path.join(dest, file);
@@ -151,8 +239,31 @@ function copyDirectory(src, dest) {
} }
// Runtime validation of critical binaries // Runtime validation of critical binaries
if (!userYtDlp){ logger.error("Missing yt-dlp binary")} // if (!userYtDlp) {
if (!ffmpegPath){ logger.error("Missing ffmpeg binary")} // getLogger().error("Missing yt-dlp binary");
if (!denoPath){ logger.error("Missing deno binary")} // }
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder, userThemesPath, initUserThemes }; // if (!ffmpegPath) {
// getLogger().error("Missing ffmpeg binary");
// }
// if (!denoPath) {
// getLogger().error("Missing deno binary");
// }
function validateBinaries() {
if (!userYtDlp) getLogger().error("Missing yt-dlp binary");
if (!ffmpegPath) getLogger().error("Missing ffmpeg binary");
if (!denoPath) getLogger().error("Missing deno binary");
}
/**
* Helper to lazy load logger, avoid circular import
* @returns {winston.Logger}
*/
function getLogger() {
return require("../logger.js").logger;
}
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder, userThemesPath, initUserThemes, isWindows, validateBinaries };

View File

@@ -4,9 +4,11 @@ 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");
const {isWindows} = require("./helpers/path.helpers");
const { logSystemInfo } = require("./sysinfo");
// Logs folder in Windows // Logs folder in Windows
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs"); const logDir = isWindows ? path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs") : path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"), "FreedomLoader", "logs");
// Create "Logs" folder if needed // Create "Logs" folder if needed
try { try {
@@ -20,6 +22,31 @@ const logFormat = format.combine(
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`) format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
); );
/**
* Format used by the saved file
* @type {Format}
*/
const fileFormat = format.combine(
format.timestamp({format: "YYYY-MM-DD HH:mm:ss"}),
format.errors({stack: true}), // Capture la stack trace complète des erreurs
format.printf(({timestamp, level, message, stack}) => {
return `${timestamp} | ${level.toUpperCase()} | ${stack || message}`;
})
);
/**
* Formated used by the IDE/Console
* @type {Format}
*/
const consoleFormat = format.combine(
format.colorize({all: true}),
format.timestamp({format: "HH:mm:ss"}),
format.errors({stack: true}),
format.printf(({timestamp, level, message, stack}) => {
return `[${timestamp}] ${level}: ${stack || message}`;
})
);
/** /**
* Logger Instance with differents types * Logger Instance with differents types
* - info * - info
@@ -31,7 +58,6 @@ const logFormat = format.combine(
*/ */
const logger = createLogger({ const logger = createLogger({
level: "info", level: "info",
format: logFormat,
transports: [ transports: [
new DailyRotateFile({ new DailyRotateFile({
dirname: logDir, dirname: logDir,
@@ -39,11 +65,11 @@ const logger = createLogger({
datePattern: "YYYY-MM-DD", datePattern: "YYYY-MM-DD",
zippedArchive: false, zippedArchive: false,
maxFiles: "7d", maxFiles: "7d",
format: logFormat, format: fileFormat,
options: { flags: "a" }, options: { flags: "a" },
}), }),
new transports.Console({ new transports.Console({
format: logFormat, format: consoleFormat,
}), }),
], ],
}); });
@@ -51,16 +77,18 @@ const logger = createLogger({
/** /**
* Start Log Session * Start Log Session
*/ */
function logSessionStart() { function logSessionStart(logDir, downloadPath) {
logger.info(`--- Starting session: ${new Date().toISOString()} ---`); logger.info(`--- Starting session: ${new Date().toLocaleString('sv-SE')} ---`);
logger.info("============================================================")
logger.info(`Application Version: ${config.version}`) logger.info(`Application Version: ${config.version}`)
if (typeof logSystemInfo === 'function') logSystemInfo(logger, logDir, downloadPath);
} }
/** /**
* Stop Log Session * Stop Log Session
*/ */
function logSessionEnd() { function logSessionEnd() {
logger.info(`--- Ending session: ${new Date().toISOString()} ---`); logger.info(`--- Ending session: ${new Date().toLocaleString('sv-SE')} ---`);
} }
module.exports = { module.exports = {

View File

@@ -2,6 +2,6 @@ const express = require("express");
const router = express.Router(); const router = express.Router();
const { infoController } = require("../controller/info.controller"); const { infoController } = require("../controller/info.controller");
router.post("/", infoController); router.get("/", infoController);
module.exports = router; module.exports = router;

View File

@@ -13,7 +13,9 @@ app.use(express.static(path.join(__dirname, "../public")));
// Routes // Routes
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"));
// Interface
app.get("/", rateLimit, (req, res) => { app.get("/", rateLimit, (req, res) => {
res.sendFile(path.join(__dirname, "../public/index.html")); res.sendFile(path.join(__dirname, "../public/index.html"));
}); });

View File

@@ -6,7 +6,7 @@ const { buildYtDlpArgs } = require("../helpers/buildArgs.helpers");
const notify = require("../helpers/notify.helpers"); 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 { configFeatures } = require("../../config.js"); const {reloadFeatures} = require("../../config");
/** /**
* Reference to the currently running yt-dlp process. * Reference to the currently running yt-dlp process.
@@ -123,7 +123,7 @@ function createPlaylistFolder(basePath, playlistTitle) {
* @param {string} [options.outputFolder] - Destination folder. * @param {string} [options.outputFolder] - Destination folder.
* @param {string} [options.playlistTitle] - Playlist title. * @param {string} [options.playlistTitle] - Playlist title.
* *
* @param {Function[]} listeners - Progress listeners. * @param {Function[]} progressListeners - Progress listeners.
* @param {Function[]} speedListeners - Download speed listeners. * @param {Function[]} speedListeners - Download speed listeners.
* @param {Function[]} stageListeners - Processing stage listeners. * @param {Function[]} stageListeners - Processing stage listeners.
* @param {Function[]} playlistInfoListeners - Playlist progress listeners. * @param {Function[]} playlistInfoListeners - Playlist progress listeners.
@@ -132,10 +132,13 @@ function createPlaylistFolder(basePath, playlistTitle) {
* Resolves with the final output folder path once the * Resolves with the final output folder path once the
* download has completed successfully. * download has completed successfully.
*/ */
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) { function fetchDownload(options, progressListeners, speedListeners, stageListeners, playlistInfoListeners) {
return new Promise((resolve, reject) => { 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; let outputFolder = options.outputFolder || defaultDownloadFolder;
@@ -159,7 +162,7 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
// 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 || isPlaylistUrl(options.url);
if (isPlaylist && configFeatures.createPlaylistFolders) { if (isPlaylist && userConfig.createPlaylistFolders) {
try { try {
const playlistName = options.playlistTitle || "Untitled Playlist"; const playlistName = options.playlistTitle || "Untitled Playlist";
safeOutputFolder = createPlaylistFolder(safeOutputFolder, playlistName); safeOutputFolder = createPlaylistFolder(safeOutputFolder, playlistName);
@@ -168,11 +171,16 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
logger.error(`Failed to create playlist folder: ${err.message}`); logger.error(`Failed to create playlist folder: ${err.message}`);
return reject(err); return reject(err);
} }
} else if (isPlaylist && !configFeatures.createPlaylistFolders) { } else if (isPlaylist && !userConfig.createPlaylistFolders) {
logger.info(`Playlist detected but createPlaylistFolders is disabled, using base folder`); logger.info(`Playlist detected but createPlaylistFolders is disabled, using base folder`);
} }
const args = buildYtDlpArgs({ ...options, outputFolder: safeOutputFolder }); const args = buildYtDlpArgs({
...options,
outputFolder: safeOutputFolder,
isPlaylist
});
logger.info(`[yt-dlp args] ${args.join(" ")}`); logger.info(`[yt-dlp args] ${args.join(" ")}`);
const child = execFile(userYtDlp, args); const child = execFile(userYtDlp, args);
@@ -182,7 +190,7 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
child.on("close", code => { child.on("close", code => {
currentDownloadProcess = null; currentDownloadProcess = null;
listeners.forEach(fn => fn("done")); progressListeners.forEach(fn => fn("done"));
if (code === 0) resolve(safeOutputFolder); if (code === 0) resolve(safeOutputFolder);
else if (code === null) reject(new Error(`Download cancelled by user`)); else if (code === null) reject(new Error(`Download cancelled by user`));
else reject(new Error(`YT-DLP failed with code : ${code}`)); else reject(new Error(`YT-DLP failed with code : ${code}`));
@@ -196,11 +204,11 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
// Progress Bar // Progress Bar
if (line.startsWith("[download] Destination:")) { if (line.startsWith("[download] Destination:")) {
listeners.forEach(fn => fn("reset")); progressListeners.forEach(fn => fn("reset"));
stageListeners.forEach(fn => fn("Downloading...")); stageListeners.forEach(fn => fn("Downloading..."));
} }
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) progressListeners.forEach(fn => fn(parseFloat(match[1])));
if (line.includes("MiB/s") || line.includes("KiB/s")) { if (line.includes("MiB/s") || line.includes("KiB/s")) {
const match = line.match(/\d+(\.\d+)?[KM]?iB\/s/); const match = line.match(/\d+(\.\d+)?[KM]?iB\/s/);

142
server/sysinfo.js Normal file
View File

@@ -0,0 +1,142 @@
const os = require("os");
const fs = require("fs");
const { execSync } = require("child_process");
const { app } = require("electron");
/**
* Reads /etc/os-release to get the Linux distribution name
*/
function getLinuxDistro() {
try {
const content = fs.readFileSync("/etc/os-release", "utf-8");
const pretty = content.match(/^PRETTY_NAME="?(.+?)"?$/m);
if (pretty) return pretty[1];
const name = content.match(/^NAME="?(.+?)"?$/m);
return name ? name[1] : "Unknown Linux";
} catch {
return "Unknown Linux";
}
}
/**
* Reads the package-type file written by electron-builder
* to determine how the app was distributed (rpm, snap, AppImage, deb...)
*/
function getPackageType() {
try {
const pkgTypePath = require("path").join(process.resourcesPath, "package-type");
if (fs.existsSync(pkgTypePath)) {
return fs.readFileSync(pkgTypePath, "utf-8").trim();
}
} catch { /* ignore */ }
return "unknown";
}
/**
* Checks if Firefox is installed on the current platform
*/
function detectFirefox() {
const platform = process.platform;
if (platform === "win32") {
const paths = [
"C:\\Program Files\\Mozilla Firefox\\firefox.exe",
"C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe",
`${process.env.LOCALAPPDATA}\\Mozilla Firefox\\firefox.exe`,
];
return paths.some(p => fs.existsSync(p));
}
if (platform === "linux") {
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`,
];
if (paths.some(p => fs.existsSync(p))) return true;
try { execSync("which firefox", { stdio: "ignore" }); return true; } catch { return false; }
}
return false;
}
/**
* Builds the system info object logged at startup
*
* @param {string} logDir - path to the log directory
* @param {string} downloadPath - current configured download path
*/
function getSystemInfo(logDir, downloadPath) {
const platform = process.platform;
const cpus = os.cpus();
const totalMem = (os.totalmem() / 1024 / 1024 / 1024).toFixed(1);
const freeMem = (os.freemem() / 1024 / 1024 / 1024).toFixed(1);
let platformLabel;
if (platform === "win32") {
platformLabel = `Windows (${os.release()})`;
} else if (platform === "linux") {
platformLabel = `Linux — ${getLinuxDistro()}`;
} else {
platformLabel = `${platform} ${os.release()} (unsupported)`;
}
const info = {
"Platform": platformLabel,
"Architecture": os.arch(),
"CPU": cpus.length ? `${cpus[0].model.trim()} (${cpus.length} cores)` : "Unknown",
"Memory": `${freeMem} GB free / ${totalMem} GB total`,
"Locale": app.getLocale(),
"Electron": process.versions.electron,
"Node": process.versions.node,
"Packaged": String(app.isPackaged),
"Package type": app.isPackaged ? getPackageType() : "dev",
"Firefox present": detectFirefox() ? "Yes" : "No — cookie features unavailable",
"Log directory": logDir,
"Download path": downloadPath,
};
// Linux-specific extras
if (platform === "linux") {
const sessionType = process.env.XDG_SESSION_TYPE || "unknown";
const waylandDisplay = process.env.WAYLAND_DISPLAY;
const x11Display = process.env.DISPLAY;
info["Display server"] = waylandDisplay
? `Wayland (${waylandDisplay})`
: x11Display
? `X11 (${x11Display})`
: sessionType !== "unknown"
? sessionType
: "unknown";
info["Desktop env"] = process.env.XDG_CURRENT_DESKTOP || process.env.DESKTOP_SESSION || "unknown";
}
return info;
}
/**
* Logs system info at application startup
*
* @param {object} logger - winston logger instance
* @param {string} logDir - path to the log directory
* @param {string} downloadPath - current configured download path
*/
function logSystemInfo(logger, logDir, downloadPath) {
const info = getSystemInfo(logDir, downloadPath);
const maxKeyLen = Math.max(...Object.keys(info).map(k => k.length));
logger.info("=".repeat(60));
logger.info("SYSTEM INFO");
logger.info("=".repeat(60));
for (const [key, val] of Object.entries(info)) {
logger.info(` ${key.padEnd(maxKeyLen)} : ${val}`);
}
logger.info("=".repeat(60));
}
module.exports = { logSystemInfo };

182
test/unit/buildArgs.test.js Normal file
View 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";
});
});

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

BIN
theme/dark.zip Normal file

Binary file not shown.

BIN
theme/light.zip Normal file

Binary file not shown.