mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-09-28 04:21:15 +02:00
Compare commits
53 Commits
1.5.1
...
7ab23d495c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ab23d495c | ||
|
|
331890f743 | ||
|
|
e398f6866a | ||
|
|
5f28aa502a | ||
|
|
4a10ef2614 | ||
|
|
1296199f65 | ||
|
|
764b566699 | ||
|
|
0c568c1e8d | ||
|
|
dae4dd852f | ||
|
|
e5c5542750 | ||
|
|
9b2c6c9552 | ||
|
|
871ebe2498 | ||
|
|
730069a3c6 | ||
|
|
7a0cbd7456 | ||
|
|
5d676b45e7 | ||
|
|
a7f3d84a7d | ||
|
|
fbce5e5b7b | ||
|
|
cc7cc38392 | ||
|
|
6a9e5a063b | ||
|
|
cf8c95711c | ||
|
|
3683e4245f | ||
|
|
01affd8ddf | ||
|
|
faba3f516b | ||
|
|
c1d9448a2b | ||
|
|
1bff8376b4 | ||
|
|
3f44c01bd7 | ||
|
|
d0bdd887e3 | ||
|
|
609ac2eadb | ||
|
|
c484d322bf | ||
|
|
b321a1d164 | ||
|
|
89e6fcf9bc | ||
|
|
ba2f317551 | ||
|
|
553e383c94 | ||
|
|
89cc0b85a3 | ||
|
|
abd0bf9006 | ||
|
|
653f73805e | ||
|
|
4f03c5876f | ||
|
|
a5f05f32af | ||
|
|
0d1b1e4919 | ||
|
|
0e77a94ec7 | ||
|
|
7f481d20b2 | ||
|
|
1ff32ca021 | ||
|
|
90d8a9f65d | ||
|
|
d5965c2ce0 | ||
|
|
15f59a6093 | ||
|
|
8b31436e25 | ||
|
|
7d5203001b | ||
|
|
60709c4fad | ||
|
|
2fb6509edc | ||
|
|
eebce83976 | ||
|
|
a56a070bc8 | ||
|
|
e1ef7032bb | ||
|
|
e7333b7b6b |
27
.gitignore
vendored
27
.gitignore
vendored
@@ -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
96
BINARIES.md
Normal 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.
|
||||||
@@ -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.)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
170
README.md
170
README.md
@@ -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**
|
||||||
|
|
||||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
[](https://masteracnolo.github.io/Freedom-Loader-Site/)
|
[](https://masteracnolo.github.io/Freedom-Loader-Site/)
|
||||||
[](https://masteracnolo.github.io/Freedom-Loader-Workshop/)
|
[](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
|
||||||
|
|
||||||
|
[](https://copr.fedorainfracloud.org/coprs/masteracnolo/freedom-loader/)
|
||||||
|
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
|
[](https://snapcraft.io/freedom-loader)
|
||||||
|
[](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,12 +236,14 @@ 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"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -192,8 +251,9 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
|||||||
### 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 |
|
||||||
|
| `systemTray` | boolean | `true` | Allow app to minimize on close |
|
||||||
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
|
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
|
||||||
| `customTopBar` | boolean | `true` | Use custom application top bar |
|
| `customTopBar` | boolean | `true` | Use custom application top bar |
|
||||||
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
|
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
|
||||||
@@ -201,13 +261,14 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
|||||||
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
||||||
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
||||||
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
||||||
|
| `keepPlaylistOrder` | boolean | `false` | Add index before title, usefull for album |
|
||||||
| `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist |
|
| `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist |
|
||||||
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
||||||
| `logSystem` | boolean | `true` | Enable application logging |
|
| `logSystem` | boolean | `true` | Enable application logging |
|
||||||
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
||||||
| `downloadSystem` | boolean | `true` | Enable download system |
|
| `downloadSystem` | boolean | `true` | Enable download system |
|
||||||
| `notifySystem` | boolean | `true` | Enable system notifications |
|
| `notifySystem` | boolean | `true` | Enable system notifications |
|
||||||
| `enableHardwareAcceleration` | boolean | `true` | Enable HArdware Acceleration |
|
| `enableHardwareAcceleration` | boolean | `true` | Enable Hardware Acceleration |
|
||||||
| `theme` | string | `dark` | Current Theme Name |
|
| `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
|
||||||
|
|||||||
69
SECURITY.md
69
SECURITY.md
@@ -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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
|
|
||||||
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");
|
||||||
@@ -28,6 +27,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 +35,7 @@ const FEATURE_WHITELIST = new Set([
|
|||||||
"addMetadata",
|
"addMetadata",
|
||||||
"verboseLogs",
|
"verboseLogs",
|
||||||
"autoDownloadPlaylist",
|
"autoDownloadPlaylist",
|
||||||
|
"keepPlaylistOrder",
|
||||||
"customCodec",
|
"customCodec",
|
||||||
"theme",
|
"theme",
|
||||||
"createPlaylistFolders",
|
"createPlaylistFolders",
|
||||||
|
|||||||
@@ -62,10 +62,19 @@ function validateDownloadPath(userPath) {
|
|||||||
* Required to prevent path traversal or alias bypass.
|
* Required to prevent path traversal or alias bypass.
|
||||||
*/
|
*/
|
||||||
const resolved = fs.realpathSync(path.resolve(userPath));
|
const resolved = fs.realpathSync(path.resolve(userPath));
|
||||||
|
|
||||||
|
if (!fs.existsSync(resolved)) {
|
||||||
|
fs.mkdirSync(resolved, { recursive: true });
|
||||||
|
logger.info(`Download folder created: ${resolved}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const real = fs.realpathSync(resolved);
|
||||||
|
|
||||||
if (!isSafePath(resolved)) {
|
if (!isSafePath(resolved)) {
|
||||||
throw new Error("Path not allowed: system folders are blocked!");
|
throw new Error("Path not allowed: system folders are blocked!");
|
||||||
}
|
}
|
||||||
return resolved;
|
|
||||||
|
return real;
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
|
||||||
|
|||||||
@@ -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
108
app/tray.js
Normal 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};
|
||||||
@@ -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) {
|
||||||
|
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
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
BIN
build/app-icon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
26
config.js
26
config.js
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"autoUpdate": true,
|
"autoUpdate": true,
|
||||||
|
"systemTray": true,
|
||||||
"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
21
jest.config.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module.exports = {
|
||||||
|
testEnvironment: "node",
|
||||||
|
testMatch: ["**/test/unit/**/*.test.js"],
|
||||||
|
collectCoverageFrom: [
|
||||||
|
"server/**/*.js",
|
||||||
|
"app/**/*.js",
|
||||||
|
"!app/autoUpdater.js",
|
||||||
|
],
|
||||||
|
|
||||||
|
modulePathIgnorePatterns: [
|
||||||
|
"<rootDir>/dist",
|
||||||
|
"<rootDir>/release",
|
||||||
|
"<rootDir>/build",
|
||||||
|
],
|
||||||
|
|
||||||
|
watchPathIgnorePatterns: [
|
||||||
|
"<rootDir>/dist",
|
||||||
|
"<rootDir>/release",
|
||||||
|
"<rootDir>/build",
|
||||||
|
],
|
||||||
|
};
|
||||||
22
main.js
22
main.js
@@ -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,10 @@ 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");
|
||||||
|
|
||||||
|
app.isQuitting = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If another instance want to run
|
* If another instance want to run
|
||||||
@@ -99,7 +101,7 @@ app.on("second-instance", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
logSessionStart();
|
logSessionStart(logDir, defaultDownloadFolder);
|
||||||
|
|
||||||
createSplashWindow();
|
createSplashWindow();
|
||||||
|
|
||||||
@@ -116,6 +118,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 +129,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,11 +145,17 @@ app.whenReady().then(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
|
if (app.isQuitting) {
|
||||||
logger.info("Shutting down...");
|
logger.info("Shutting down...");
|
||||||
app.quit();
|
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", async () => {
|
||||||
|
app.isQuitting = true;
|
||||||
|
destroyTray();
|
||||||
await stopRPC();
|
await stopRPC();
|
||||||
logger.info("All services stopped. Have a nice day!");
|
logger.info("All services stopped. Have a nice day!");
|
||||||
logSessionEnd();
|
logSessionEnd();
|
||||||
|
|||||||
6141
package-lock.json
generated
6141
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
143
package.json
143
package.json
@@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
package/com.masteracnolo.freedomloader.metainfo.xml
Normal file
73
package/com.masteracnolo.freedomloader.metainfo.xml
Normal 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
9
package/copr-build.sh
Normal 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"
|
||||||
25
package/freedom-loader.spec
Normal file
25
package/freedom-loader.spec
Normal 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
|
||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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,6 +127,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<input data-key="systemTray" id="systemTray" type="checkbox">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">System Tray</span>
|
||||||
|
<small>Minimize app on close, in the System Tray.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- <div class="setting-item">
|
<!-- <div class="setting-item">
|
||||||
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
|
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
@@ -135,6 +143,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<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">
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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 }));
|
||||||
|
|||||||
27
public/script/customConfiguration.js
Normal file
27
public/script/customConfiguration.js
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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 }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
if (!res.ok) return {error: `An Error occured when fetching info`};
|
if (!res.ok) return {error: `An Error occured when fetching info`};
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
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();
|
||||||
@@ -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
BIN
resources/binaries/linux/yt-dlp
Executable file
Binary file not shown.
116
scripts/create-copr-package.sh
Executable file
116
scripts/create-copr-package.sh
Executable 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
187
scripts/release.sh
Executable 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 "============================================"
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -62,7 +62,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 +70,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 +118,19 @@ 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";
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push("-o", path.join(outputFolder, outputTemplate));
|
||||||
args.push(url);
|
args.push(url);
|
||||||
|
|
||||||
return args.filter(Boolean);
|
return args.filter(Boolean);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
// 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();
|
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
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(destPath)) {
|
|
||||||
try {
|
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}`);
|
}
|
||||||
|
getLogger().info(`Copied default theme: ${theme}`);
|
||||||
} catch (copyErr) {
|
} catch (copyErr) {
|
||||||
logger.warn(`Failed to copy theme ${theme}: ${copyErr.message}. Theme folder will be created, add theme files manually.`);
|
getLogger().warn(`Failed to copy theme ${theme}: ${copyErr.message}`);
|
||||||
try {
|
|
||||||
fs.mkdirSync(destPath, { recursive: true });
|
|
||||||
fs.chmodSync(destPath, 0o777);
|
|
||||||
} catch (mkdirErr) {
|
|
||||||
logger.warn(`Could not create theme directory ${theme}: ${mkdirErr.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 };
|
||||||
@@ -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,9 +77,11 @@ 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().toISOString()} ---`);
|
||||||
|
logger.info("============================================================")
|
||||||
logger.info(`Application Version: ${config.version}`)
|
logger.info(`Application Version: ${config.version}`)
|
||||||
|
if (typeof logSystemInfo === 'function') logSystemInfo(logger, logDir, downloadPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -14,6 +14,8 @@ 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"));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -135,7 +135,10 @@ function createPlaylistFolder(basePath, playlistTitle) {
|
|||||||
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) {
|
function fetchDownload(options, listeners, 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);
|
||||||
|
|||||||
142
server/sysinfo.js
Normal file
142
server/sysinfo.js
Normal 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
182
test/unit/buildArgs.test.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
jest.mock("../../server/helpers/getBrowser.helpers.js", () =>
|
||||||
|
jest.fn(() => "firefox")
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.mock("../../server/helpers/path.helpers.js", () => ({
|
||||||
|
ffmpegPath: "/mock/ffmpeg",
|
||||||
|
denoPath: "/mock/deno",
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../../config.js", () => ({
|
||||||
|
configFeatures: {
|
||||||
|
autoUpdate: false,
|
||||||
|
discordRPC: false,
|
||||||
|
customTopBar: false,
|
||||||
|
autoCheckInfo: false,
|
||||||
|
addThumbnail: true,
|
||||||
|
addMetadata: true,
|
||||||
|
verboseLogs: false,
|
||||||
|
autoDownloadPlaylist: false,
|
||||||
|
customCodec: "h264",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../../server/logger.js", () => ({
|
||||||
|
logger: {
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { buildYtDlpArgs } = require("../../server/helpers/buildArgs.helpers");
|
||||||
|
const { configFeatures } = require("../../config");
|
||||||
|
const { logger } = require("../../server/logger");
|
||||||
|
|
||||||
|
describe("buildYtDlpArgs", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("builds default video arguments", () => {
|
||||||
|
const args = buildYtDlpArgs({
|
||||||
|
url: "https://youtube.com/watch?v=123",
|
||||||
|
audioOnly: false,
|
||||||
|
quality: "best",
|
||||||
|
outputFolder: "/downloads",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(args).toContain("--cookies-from-browser");
|
||||||
|
expect(args).toContain("firefox");
|
||||||
|
|
||||||
|
expect(args).toContain("--ffmpeg-location");
|
||||||
|
expect(args).toContain("/mock/ffmpeg");
|
||||||
|
|
||||||
|
expect(args).toContain("--js-runtimes");
|
||||||
|
expect(args).toContain("deno:/mock/deno");
|
||||||
|
|
||||||
|
expect(args).toContain("--merge-output");
|
||||||
|
expect(args).toContain("mp4");
|
||||||
|
|
||||||
|
expect(args).toContain("-f");
|
||||||
|
expect(args).toContain("bestvideo+bestaudio/best/mp4");
|
||||||
|
|
||||||
|
expect(args).toContain(
|
||||||
|
path.join("/downloads", "%(title)s.%(ext)s")
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(args.at(-1)).toBe("https://youtube.com/watch?v=123");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("builds audio-only arguments", () => {
|
||||||
|
const args = buildYtDlpArgs({
|
||||||
|
url: "https://youtube.com/watch?v=123",
|
||||||
|
audioOnly: true,
|
||||||
|
quality: "best",
|
||||||
|
outputFolder: "/downloads",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(args).toContain("--extract-audio");
|
||||||
|
expect(args).toContain("--audio-format");
|
||||||
|
expect(args).toContain("mp3");
|
||||||
|
expect(args).toContain("bestaudio");
|
||||||
|
|
||||||
|
expect(args).not.toContain("--merge-output");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses requested quality", () => {
|
||||||
|
const args = buildYtDlpArgs({
|
||||||
|
url: "https://youtube.com/watch?v=123",
|
||||||
|
audioOnly: false,
|
||||||
|
quality: "720",
|
||||||
|
outputFolder: "/downloads",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(args).toContain(
|
||||||
|
"bestvideo[height<=720]+bestaudio/best[height<=720]/mp4"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("falls back to best for unknown quality", () => {
|
||||||
|
const args = buildYtDlpArgs({
|
||||||
|
url: "https://youtube.com/watch?v=123",
|
||||||
|
audioOnly: false,
|
||||||
|
quality: "unknown",
|
||||||
|
outputFolder: "/downloads",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(args).toContain("best");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses --no-playlist by default", () => {
|
||||||
|
const args = buildYtDlpArgs({
|
||||||
|
url: "https://youtube.com/watch?v=123",
|
||||||
|
audioOnly: false,
|
||||||
|
quality: "best",
|
||||||
|
outputFolder: "/downloads",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(args).toContain("--no-playlist");
|
||||||
|
expect(args).not.toContain("--yes-playlist");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses --yes-playlist when enabled", () => {
|
||||||
|
configFeatures.autoDownloadPlaylist = true;
|
||||||
|
|
||||||
|
const args = buildYtDlpArgs({
|
||||||
|
url: "https://youtube.com/watch?v=123",
|
||||||
|
audioOnly: false,
|
||||||
|
quality: "best",
|
||||||
|
outputFolder: "/downloads",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(args).toContain("--yes-playlist");
|
||||||
|
expect(args).not.toContain("--no-playlist");
|
||||||
|
|
||||||
|
configFeatures.autoDownloadPlaylist = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("embeds thumbnail and metadata when enabled", () => {
|
||||||
|
const args = buildYtDlpArgs({
|
||||||
|
url: "https://youtube.com/watch?v=123",
|
||||||
|
audioOnly: false,
|
||||||
|
quality: "best",
|
||||||
|
outputFolder: "/downloads",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(args).toContain("--embed-thumbnail");
|
||||||
|
expect(args).toContain("--add-metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logs codec validation", () => {
|
||||||
|
buildYtDlpArgs({
|
||||||
|
url: "https://youtube.com/watch?v=123",
|
||||||
|
audioOnly: false,
|
||||||
|
quality: "best",
|
||||||
|
outputFolder: "/downloads",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(logger.info).toHaveBeenCalledWith("Codec Valid: h264");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("falls back to h264 when codec is invalid", () => {
|
||||||
|
configFeatures.customCodec = "invalid";
|
||||||
|
|
||||||
|
const args = buildYtDlpArgs({
|
||||||
|
url: "https://youtube.com/watch?v=123",
|
||||||
|
audioOnly: false,
|
||||||
|
quality: "best",
|
||||||
|
outputFolder: "/downloads",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
|
"Codec not valid: invalid. Using default codec"
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortIndex = args.indexOf("-S");
|
||||||
|
expect(args[sortIndex + 1]).toContain("vcodec:h264");
|
||||||
|
|
||||||
|
configFeatures.customCodec = "h264";
|
||||||
|
});
|
||||||
|
});
|
||||||
133
test/unit/notify.helpers.test.js
Normal file
133
test/unit/notify.helpers.test.js
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
const mockShow = jest.fn();
|
||||||
|
const mockOn = jest.fn();
|
||||||
|
const mockOpenPath = jest.fn();
|
||||||
|
const mockOpenExternal = jest.fn();
|
||||||
|
|
||||||
|
jest.mock("electron", () => ({
|
||||||
|
Notification: jest.fn().mockImplementation(() => ({
|
||||||
|
show: mockShow,
|
||||||
|
on: mockOn,
|
||||||
|
})),
|
||||||
|
shell: {
|
||||||
|
openPath: mockOpenPath,
|
||||||
|
openExternal: mockOpenExternal,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../../server/helpers/path.helpers", () => ({
|
||||||
|
iconPaths: {
|
||||||
|
confirm: "/icons/confirm.png",
|
||||||
|
error: "/icons/error.png",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../../server/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { Notification, shell } = require("electron");
|
||||||
|
|
||||||
|
const {
|
||||||
|
notifyDownloadFinished,
|
||||||
|
notifyCookiesBrowserError,
|
||||||
|
notifyFirefoxBrowserMissing,
|
||||||
|
} = require("../../server/helpers/notify.helpers");
|
||||||
|
|
||||||
|
describe("notify.helpers", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("notifyDownloadFinished", () => {
|
||||||
|
test("does nothing when notifications are disabled", () => {
|
||||||
|
notifyDownloadFinished("/downloads", false);
|
||||||
|
|
||||||
|
expect(Notification).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does nothing when folder is missing", () => {
|
||||||
|
notifyDownloadFinished(undefined, true);
|
||||||
|
|
||||||
|
expect(Notification).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates and shows notification", () => {
|
||||||
|
notifyDownloadFinished("/downloads");
|
||||||
|
|
||||||
|
expect(Notification).toHaveBeenCalledWith({
|
||||||
|
title: "Freedom Loader",
|
||||||
|
body: "Your download is complete, click here to open it.",
|
||||||
|
icon: "/icons/confirm.png",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockOn).toHaveBeenCalledWith(
|
||||||
|
"click",
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockShow).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("opens folder when notification is clicked", () => {
|
||||||
|
notifyDownloadFinished("/downloads");
|
||||||
|
|
||||||
|
const callback = mockOn.mock.calls[0][1];
|
||||||
|
callback();
|
||||||
|
|
||||||
|
expect(shell.openPath).toHaveBeenCalledWith("/downloads");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("notifyCookiesBrowserError", () => {
|
||||||
|
test("creates notification", () => {
|
||||||
|
notifyCookiesBrowserError();
|
||||||
|
|
||||||
|
expect(Notification).toHaveBeenCalledWith({
|
||||||
|
title: "Cookies Error",
|
||||||
|
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.",
|
||||||
|
icon: "/icons/error.png",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockShow).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("opens tutorial when clicked", () => {
|
||||||
|
notifyCookiesBrowserError();
|
||||||
|
|
||||||
|
const callback = mockOn.mock.calls[0][1];
|
||||||
|
callback();
|
||||||
|
|
||||||
|
expect(shell.openExternal).toHaveBeenCalledWith(
|
||||||
|
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("notifyFirefoxBrowserMissing", () => {
|
||||||
|
test("creates notification", () => {
|
||||||
|
notifyFirefoxBrowserMissing();
|
||||||
|
|
||||||
|
expect(Notification).toHaveBeenCalledWith({
|
||||||
|
title: "Firefox Missing",
|
||||||
|
body: "Firefox was not found on your system. Click here to follow the installation guide",
|
||||||
|
icon: "/icons/error.png",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockShow).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("opens installation guide when clicked", () => {
|
||||||
|
notifyFirefoxBrowserMissing();
|
||||||
|
|
||||||
|
const callback = mockOn.mock.calls[0][1];
|
||||||
|
callback();
|
||||||
|
|
||||||
|
expect(shell.openExternal).toHaveBeenCalledWith(
|
||||||
|
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
BIN
theme/dark.zip
Normal file
BIN
theme/dark.zip
Normal file
Binary file not shown.
BIN
theme/light.zip
Normal file
BIN
theme/light.zip
Normal file
Binary file not shown.
Reference in New Issue
Block a user