Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4312376bb | ||
|
|
5ffb143a1a | ||
|
|
ee24473077 | ||
|
|
2b98a63f92 | ||
|
|
2e594d87e8 | ||
|
|
b343c2869d | ||
|
|
094b93f4fa | ||
|
|
cdc8a623c0 | ||
|
|
2b43c00a3a | ||
|
|
14928798d4 | ||
|
|
e5fb44fc6b | ||
|
|
9f9e4cd1d8 | ||
|
|
45fe7336fe | ||
|
|
b909e7d917 | ||
|
|
ce62748ade | ||
|
|
d4ecc36f6b | ||
|
|
fee364610e | ||
|
|
492af8525b | ||
|
|
fb58b5ae06 | ||
|
|
577034b0bd | ||
|
|
6fc452edb4 | ||
|
|
086ca49599 | ||
|
|
be1de35da8 | ||
|
|
3c1f57667f | ||
|
|
5997cd20b4 | ||
|
|
6220c9400e | ||
|
|
48a16b927f | ||
|
|
87545d77b4 | ||
|
|
4e0fe29c20 | ||
|
|
f8e3bc843d | ||
|
|
b3f1edaabd | ||
|
|
ef784dd19c | ||
|
|
69bbcee885 | ||
|
|
60b4a99506 | ||
|
|
c1030400f5 | ||
|
|
3f508da501 | ||
|
|
0437cb61a2 | ||
|
|
1372582c54 | ||
|
|
070c3d507f | ||
|
|
3e9c545952 | ||
|
|
e3845e1849 | ||
|
|
91703d6a91 | ||
|
|
cff3101b03 | ||
|
|
c9a48ec64c | ||
|
|
e6419a9759 | ||
|
|
9d1d2fc7a8 | ||
|
|
cdca4a82ab | ||
|
|
c2e4e12512 | ||
|
|
96c1c6717d | ||
|
|
abf776175c | ||
|
|
d6de184bfd | ||
|
|
12249c4e8a | ||
|
|
f1fbc3f0c7 | ||
|
|
bb8cf54bf9 | ||
|
|
dccf8bd786 | ||
|
|
793bb79d76 | ||
|
|
f5cdf27ce6 | ||
|
|
627c56cd25 | ||
|
|
783871d131 | ||
|
|
138c05f995 | ||
|
|
5009a479af | ||
|
|
2a95a10113 | ||
|
|
9d6065c80a | ||
|
|
cd8b2bf007 | ||
|
|
1faf74fbc4 | ||
|
|
09163afbcd | ||
|
|
405d141055 | ||
|
|
599e09fa9f | ||
|
|
4954de5f1a | ||
|
|
ac854ae559 | ||
|
|
9944f05139 | ||
|
|
f6558b656a | ||
|
|
bfbf57ba38 | ||
|
|
d0af35377c | ||
|
|
59c263a246 | ||
|
|
42700bd7d8 | ||
|
|
5a9b364b4b | ||
|
|
3619550c74 | ||
|
|
31cc4012ac | ||
|
|
2e329a8584 | ||
|
|
d92dd59f86 | ||
|
|
5bb7ca377d | ||
|
|
9ee9a3574c | ||
|
|
4be5c14999 | ||
|
|
b92ffebdb4 | ||
|
|
7b6e48b09e | ||
|
|
368851b4bb | ||
|
|
fa2da5712d | ||
|
|
7df97d70bb | ||
|
|
5c5f595829 | ||
|
|
5b659e3d8a | ||
|
|
1a526777cc | ||
|
|
b0d99d4c29 | ||
|
|
c7554475a9 | ||
|
|
cab177e8a7 | ||
|
|
f7244a69ac | ||
|
|
c6ed79ed1d | ||
|
|
00e5358bbb | ||
|
|
598c35eb54 | ||
|
|
00c74ea6c6 | ||
|
|
0c415c9106 | ||
|
|
b0495d033d | ||
|
|
d72cc6e0ca | ||
|
|
b59cb9f57f | ||
|
|
af743be5b0 | ||
|
|
024d039701 | ||
|
|
e9a91656bc | ||
|
|
3505606b44 | ||
|
|
815abae0a0 | ||
|
|
004271241e | ||
|
|
8295967ff2 | ||
|
|
053dca365c | ||
|
|
62110b1a5b | ||
|
|
aae9b2866c | ||
|
|
0d8bddbc20 | ||
|
|
2a7d61bab7 | ||
|
|
de71035fa0 | ||
|
|
5368fb9311 | ||
|
|
3d583a9b6f | ||
|
|
94de0a42f1 | ||
|
|
5ba2901273 | ||
|
|
f5888b8382 | ||
|
|
6ece182629 | ||
|
|
777257b71d | ||
|
|
df0156d603 | ||
|
|
68cdad06ab | ||
|
|
ee7c6520b9 | ||
|
|
4ed1609e41 | ||
|
|
b64e61d45d | ||
|
|
b4c2c865d8 | ||
|
|
e32ffd8fa0 | ||
|
|
8be6a932e5 | ||
|
|
6ed59fd272 | ||
|
|
4df1cb207f | ||
|
|
d03e3bc7e5 | ||
|
|
df02acb97f | ||
|
|
6ef04700ab | ||
|
|
766694f330 | ||
|
|
93380b2f36 | ||
|
|
8c969d694f | ||
|
|
f2f3d42e8c | ||
|
|
8d8973acfc | ||
|
|
281bff2379 | ||
|
|
e30f54050f | ||
|
|
0ae808c1a0 | ||
|
|
2598573844 | ||
|
|
27782376a3 | ||
|
|
876f7e1db0 | ||
|
|
db654c5f82 | ||
|
|
751f3d5c3e | ||
|
|
2a8d307bc8 | ||
|
|
ec6963cff3 | ||
|
|
196a07977a | ||
|
|
f20366f0c7 | ||
|
|
f54dc18e85 |
7
.gitignore
vendored
@@ -1,8 +1,11 @@
|
||||
/node_modules
|
||||
/downloads
|
||||
/dist
|
||||
/logs
|
||||
/out
|
||||
|
||||
/ressources/ffmpeg.exe
|
||||
/ressources/ffprobe.exe
|
||||
/ressources/deno.exe
|
||||
|
||||
yt-dlp.exe
|
||||
logs/*.json
|
||||
logs/*.log
|
||||
15
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"editor.quickSuggestions": {
|
||||
"comments": "off",
|
||||
"strings": "off",
|
||||
"other": "off"
|
||||
},
|
||||
"editor.suggestOnTriggerCharacters": false,
|
||||
"editor.parameterHints.enabled": false,
|
||||
"editor.codeLens": false,
|
||||
"editor.lightbulb.enabled": "off",
|
||||
"editor.inlineSuggest.enabled": false,
|
||||
"github.copilot.enable": {
|
||||
"*": false
|
||||
}
|
||||
}
|
||||
103
README-EN.md
@@ -1,103 +0,0 @@
|
||||
# Freedom Loader
|
||||
|
||||
 
|
||||
|
||||
> This project aims to enable free, open-source, and unrestricted downloading of Video/Audio content. Eventually, it will allow users to choose
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
✅ Video Download (MP4)
|
||||
✅ Audio Download (MP3)
|
||||
✅ Detailed Logs (Console + Logs)
|
||||
✅ Automatic Fetch of Information
|
||||
✅ Playlist Download
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Installation
|
||||
|
||||
To install the application, simply download the installer from the releases section. Take the latest version and run it.
|
||||
|
||||
---
|
||||
|
||||
## ❓ Frequently Asked Questions
|
||||
|
||||
### _Where are my downloaded files located?_
|
||||
|
||||
In your computer's Downloads folder (**C:\Users\YOUR NAME\Downloads**), inside a folder named **Freedom Loader Output**
|
||||
<br>
|
||||
|
||||
### _After installing and running the program, I get an error_
|
||||
|
||||
Try restarting the application, checking the logs, or opening the browser's developer console by pressing `CTRL + SHIFT + I` or by going to `View > Toggle Developer Tools`
|
||||
|
||||
<!--  -->
|
||||
<p align="center">
|
||||
<img src="build/example-developertools.png" alt="Example Developer Tools" />
|
||||
</p>
|
||||
|
||||
## 🏗️ Upcoming Features (Roadmap)
|
||||
|
||||
- [ ] More format options
|
||||
- [ ] Subtitle support
|
||||
- [ ] Improved UI / UX
|
||||
- [ ] Language selection
|
||||
- [ ] Download a specific part of the video
|
||||
- [ ] Rename output file
|
||||
- [ ] Extract / Split video using native chapters
|
||||
- [ ] Parallel downloads
|
||||
- [ ] Skip sponsored parts of videos
|
||||
- [ ] Custom output path selection (default **C:\Users\Username\Downloads** with a **Freedom Loader Output** folder created)
|
||||
|
||||
---
|
||||
|
||||
## 🖼️ Preview
|
||||
|
||||
<p align="center">
|
||||
<img src="build/exemple.png" alt="Example Image" />
|
||||
</p>
|
||||
|
||||
> Here is an example of the app after launch and entering a URL
|
||||
|
||||
---
|
||||
|
||||
## ℹ️ About
|
||||
|
||||
This project was built using [](https://github.com/yt-dlp/yt-dlp)
|
||||
Thanks to them for providing a free and open-source API.
|
||||
|
||||
This project is open-source and welcomes your contributions.
|
||||
Here is how to get involved:
|
||||
|
||||
- **🪲 Bug Reporting**
|
||||
Please use the [GitHub issues system](https://github.com/MasterAcnolo/Freedom-Loader/issues) to report any bugs you encounter.
|
||||
Try to provide clear reproduction steps and logs if possible. Logs are located in the installation folder of the program. Example path:
|
||||
`C:\Users\[YOUR NAME]\Desktop\Freedom Loader\resources\app\logs`
|
||||
The log file will be named something like `combined-20xx-xx-xx.log`
|
||||
<br>
|
||||
- **🛠️ Contribution**
|
||||
Pull Requests are welcome! Please fork the repo and submit your PR after ensuring you meet the contribution guidelines (tests, style, etc.).
|
||||
<br>
|
||||
- **💡 Feature Suggestions**
|
||||
You can propose new ideas via a [feature request issue](https://github.com/MasterAcnolo/Freedom-Loader/issues).
|
||||
Any clear and detailed proposal will be reviewed.
|
||||
<br>
|
||||
- **💰 Support the Project**
|
||||
<p>
|
||||
If you'd like to support me, you can donate via
|
||||
<a href="https://paypal.me/axelnicolas25">
|
||||
<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white" alt="PayPal">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the **MIT License** — see the [LICENSE](./LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Disclaimer
|
||||
|
||||
I have obviously used AI assistance for some parts of this project. Use AI sparingly and always be aware of what you are doing.
|
||||
230
README.md
@@ -1,103 +1,173 @@
|
||||
# Freedom Loader
|
||||
<div align="center">
|
||||
<img
|
||||
src="./build/banner.png"
|
||||
alt="Banner"
|
||||
style="width: 50%;;"/>
|
||||
|
||||
 
|
||||
</div>
|
||||
|
||||
> Ce projet a pour but de permettre le téléchargement de contenu Vidéo/Audio de manière gratuite, open source et libre. Il permettra à terme de choisir
|
||||
|
||||
## 🚀 Fonctionnalités
|
||||
|
||||
✅ Téléchargement Vidéo (MP4)
|
||||
✅ Téléchargement Audio (MP3)
|
||||
✅ Logs détaillés (console + fichiers logs)
|
||||
✅ Récupération automatique des informations
|
||||
✅ Téléchargement des playlists
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Installation
|
||||
|
||||
Pour installer l'application, il vous suffit de télécharger l'installateur dans la section releases, de prendre la dernière version et de l'exécuter.
|
||||
|
||||
---
|
||||
|
||||
## ❓ Questions fréquentes
|
||||
|
||||
### _Où se situent mes fichiers une fois téléchargés ?_
|
||||
|
||||
Dans le dossier Téléchargements (**C:\Users\VOTRE NOM\Downloads**) de votre ordinateur, dans un dossier nommé **Freedom Loader Output**
|
||||
<div align="center">
|
||||
<a href="https://github.com/MasterAcnolo/Freedom-Loader/releases">
|
||||
<img src="https://img.shields.io/badge/Release-1.3.0-blue?style=for-the-badge">
|
||||
</a>
|
||||
<a href="https://masteracnolo.github.io/No-Sense/FreedomLoader/index.html">
|
||||
<img src="https://img.shields.io/badge/Freedom%20Loader-Open%20Page-111111?style=for-the-badge&logo=terminal&logoColor=white">
|
||||
</a>
|
||||
<a href="https://www.firefox.com/fr/?utm_campaign=SET_DEFAULT_BROWSER">
|
||||
<img src="https://img.shields.io/badge/Require Firefox-E66000?style=for-the-badge&logo=Firefox-Browser&logoColor=white">
|
||||
</a>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
### _Une fois le programme installé et lancé, j'ai une erreur_
|
||||
|
||||
Essayez de relancer l'application, de consulter les logs ou d'ouvrir la console de développement du navigateur en faisant le raccourci `CTRL + SHIFT + I` ou via `View > Toggle Developer Tools`.
|
||||
|
||||
<!--  -->
|
||||
<p align="center">
|
||||
<img src="build/example-developertools.png" alt="Exemple Developer Tools" />
|
||||
</p>
|
||||
|
||||
## 🏗️ Fonctionnalités à venir (Roadmap)
|
||||
|
||||
- [ ] Plus de choix de formats
|
||||
- [ ] Support des sous-titres
|
||||
- [ ] Meilleure UI / UX
|
||||
- [ ] Choix des langues
|
||||
- [ ] Téléchargement d’une partie de la vidéo
|
||||
- [ ] Renommage du fichier de sortie
|
||||
- [ ] Extraction / découpage de la vidéo via chapitrage natif
|
||||
- [ ] Téléchargements parallèles
|
||||
- [ ] Suppression automatique des parties sponsorisées (skip sponsor)
|
||||
- [ ] Choix personnalisé du chemin de sortie (par défaut **C:\Users\Utilisateur\Downloads**, où un dossier **Freedom Loader Output** sera créé)
|
||||
Freedom Loader is a **Windows desktop** application built with **Electron**.
|
||||
It provides a simple and reliable way to download video or audio content with metadata and advanced options, without ads or questionable third-party services.
|
||||
The goal is to offer a clean, user-friendly, and transparent download experience. It's important for me to make media content downloading accessible to people who love music or videos and don’t always have an internet connection.
|
||||
|
||||
---
|
||||
|
||||
## 🖼️ Aperçu
|
||||
## Features
|
||||
|
||||
- Video download with metadata (MP4)
|
||||
- Audio-only download with thumbnail and tags (MP3)
|
||||
- Full playlist support and better display in UI
|
||||
- Automatic metadata retrieval (title, duration, author, etc.)
|
||||
- Detailed logs (console + rotating files for 7 days)
|
||||
- Progress Bar for download and network speed indicator
|
||||
- Better UX when fetching info
|
||||
- Custom output path with persistence
|
||||
- Automatic dependency updates (YT-DLP)
|
||||
- Automatic Application Update
|
||||
- Multiple UI themes
|
||||
- Simple, responsive interface
|
||||
- Notifications on download completion (click to open folder)
|
||||
- Discord Rich Presence
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Download the latest installer from the **Releases** page and run it (Defender could stop you but click on "Run Anyway" and it's good).
|
||||
Actually, **Firefox Browser** is the only browser supported for get cookies. You should install it. Follow the tutorial in the wiki page if you need some help, Firefox Section. [Link](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html)
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Where are my downloaded files stored?
|
||||
|
||||
In your **Downloads** folder, inside **Freedom Loader**.
|
||||
Example path:
|
||||
`C:\Users\[YOUR NAME]\Downloads\Freedom Loader`
|
||||
|
||||
### I get an error after launching the app
|
||||
|
||||
Try restarting the application, checking the logs, or opening the developer console:
|
||||
`CTRL + SHIFT + I` -> “Toggle Developer Tools”.
|
||||
|
||||
<p align="center">
|
||||
<img src="build/exemple.png" alt="Exemple d'application" />
|
||||
<img src="build/example-developertools.png" width="20%" />
|
||||
</p>
|
||||
|
||||
> Voici un exemple de l’application lancée après avoir saisi une URL.
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] ~~Website for Freedom Loader~~
|
||||
- [ ] More format options
|
||||
- [ ] Linux version
|
||||
- [x] ~~Auto-updating YT-DLP~~
|
||||
- [x] ~~Automatic project updates~~
|
||||
- [x] ~~Better download status~~
|
||||
- [ ] Better website support (currently only YouTube is fully supported, other site could work but i can't prove it)
|
||||
- [ ] Subtitle support
|
||||
- [x] ~~UI/UX improvements~~
|
||||
- [ ] Language selection
|
||||
- [ ] Download specific parts of a video
|
||||
- [ ] Custom file naming
|
||||
- [ ] Parallel downloads
|
||||
- [ ] Automatic sponsor skipping
|
||||
- [x] ~~Extract/split video using native chapters~~
|
||||
- [x] ~~Custom output path selection~~
|
||||
|
||||
---
|
||||
|
||||
## ℹ️ À propos
|
||||
## Preview
|
||||
|
||||
Ce projet a été réalisé en utilisant [](https://github.com/yt-dlp/yt-dlp)
|
||||
Merci à eux de nous permettre d’utiliser leur API gratuitement et en open source.
|
||||
|
||||
Ce projet est open source et accueille volontiers vos contributions.
|
||||
Voici comment participer :
|
||||
|
||||
- **🪲 Signalement de bugs**
|
||||
Merci d’utiliser le [système d’issues GitHub](https://github.com/MasterAcnolo/Freedom-Loader/issues) pour remonter les bugs rencontrés.
|
||||
Essayez de fournir une reproduction claire et les logs si possible. Les logs se trouvent dans le dossier d’installation du programme. Exemple de chemin :
|
||||
`C:\Users\[VOTRE NOM]\Desktop\Freedom Loader\resources\app\logs`
|
||||
Le fichier aura un nom du type `combined-20xx-xx-xx.log`.
|
||||
<br>
|
||||
- **🛠️ Contribution**
|
||||
Les Pull Requests sont les bienvenues ! Merci de forker le repo et de soumettre votre PR après avoir vérifié le respect des guidelines (tests, style, etc.).
|
||||
<br>
|
||||
- **💡 Suggestions de fonctionnalités**
|
||||
Vous pouvez proposer de nouvelles idées via une [issue de type feature request](https://github.com/MasterAcnolo/Freedom-Loader/issues).
|
||||
Toute proposition claire et détaillée sera étudiée.
|
||||
<br>
|
||||
- **💰 Soutenir le projet**
|
||||
<p>
|
||||
Si vous souhaitez m’encourager, vous pouvez faire un don sur
|
||||
<a href="https://paypal.me/axelnicolas25">
|
||||
<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white" alt="PayPal">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="build/apercu1.2.4.png" width="40%" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📄 Licence
|
||||
## Technologies
|
||||
|
||||
Ce projet est sous licence **MIT** — voir le fichier [LICENSE](./LICENSE) pour plus d’informations.
|
||||
This project uses:
|
||||
|
||||
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
||||
|
||||
<a href="https://github.com/yt-dlp/yt-dlp">
|
||||
<img src="https://img.shields.io/badge/yt--dlp-Visit%20Repo-cf0000?style=for-the-badge&logo=github&logoColor=white">
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/FFmpeg/FFmpeg">
|
||||
<img src="https://img.shields.io/badge/FFmpeg-Visit%20Repo-666666?style=for-the-badge&logo=ffmpeg&logoColor=white" />
|
||||
</a>
|
||||
|
||||
<a href="https://nodejs.org/">
|
||||
<img src="https://img.shields.io/badge/Node.js-Visit%20Repo-339933?style=for-the-badge&logo=node.js&logoColor=white" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/electron/electron">
|
||||
<img src="https://img.shields.io/badge/Electron-Visit%20Repo-47848F?style=for-the-badge&logo=electron&logoColor=white" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/denoland/deno/">
|
||||
<img src="https://img.shields.io/badge/Deno-Visit%20Repo-14e0cc?style=for-the-badge&logo=deno&logoColor=white" />
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Disclaimer
|
||||
## Contributing
|
||||
|
||||
J’ai bien évidemment eu recours à l’IA pour m’aider sur certaines parties du projet. Utilisez l’IA avec parcimonie et soyez toujours conscient de ce que vous faites.
|
||||
Contributions are welcome.
|
||||
|
||||
### Bug Reports
|
||||
|
||||
Use the GitHub Issues system and include reproduction steps and logs when possible.
|
||||
Logs can be found here:
|
||||
`C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-20xx-xx-xx.log`
|
||||
|
||||
### Feature Requests
|
||||
|
||||
Open a feature request issue with a clear description and use cases.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
Fork the repository, make your changes, and submit a PR once everything is tested and clean.
|
||||
|
||||
---
|
||||
|
||||
## Support the Project
|
||||
|
||||
If you'd like to support development:
|
||||
|
||||
<a href="https://paypal.me/axelnicolas25">
|
||||
<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white" />
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Freedom Loader is released under the **GNU General Public License v3.0 (GPLv3)**.
|
||||
You are free to use, modify, and redistribute the software under the terms of this license.
|
||||
|
||||
See the full license in the [LICENSE](./LICENSE) file.
|
||||
|
||||
---
|
||||
|
||||
## Note
|
||||
|
||||
AI assistance was used occasionally to speed up development, but all code and decisions were reviewed and validated manually.
|
||||
|
||||
BIN
build/apercu1.2.4.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
build/app-icon.ico
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
build/banner.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
build/confirm-icon.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
build/error.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 5.6 KiB |
10
config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const packageJson = require("./package.json");
|
||||
const { app } = require("electron");
|
||||
|
||||
module.exports = {
|
||||
version: packageJson.version,
|
||||
applicationPort: "8787",
|
||||
debugMode: true,
|
||||
localMode: !app.isPackaged,
|
||||
DiscordRPCID: "1410934537051181146",
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
|
||||
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
|
||||
|
||||
module.exports = {
|
||||
packagerConfig: {
|
||||
asar: true,
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers: [
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-zip',
|
||||
platforms: ['darwin'],
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-deb',
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-rpm',
|
||||
config: {},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
name: '@electron-forge/plugin-auto-unpack-natives',
|
||||
config: {},
|
||||
},
|
||||
// Fuses are used to enable/disable various Electron functionality
|
||||
// at package time, before code signing the application
|
||||
new FusesPlugin({
|
||||
version: FuseVersion.V1,
|
||||
[FuseV1Options.RunAsNode]: false,
|
||||
[FuseV1Options.EnableCookieEncryption]: true,
|
||||
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
||||
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||
}),
|
||||
],
|
||||
};
|
||||
155
main.js
@@ -1,36 +1,43 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
|
||||
Freedom Loader is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
|
||||
Freedom Loader is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const { app, BrowserWindow } = require("electron");
|
||||
const config = require("./config.js");
|
||||
const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
|
||||
const path = require("path");
|
||||
const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
|
||||
const os = require("os");
|
||||
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
||||
const { AutoUpdater } = require("./server/update.js");
|
||||
|
||||
let mainWindow;
|
||||
const logsFolderPath = logDir;
|
||||
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
||||
|
||||
async function createWindow() {
|
||||
logger.info("Creation de la fenetre...");
|
||||
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
|
||||
app.disableHardwareAcceleration();
|
||||
|
||||
// Gestion single instance
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
|
||||
if (!gotLock) {
|
||||
// Une instance existe déjà -> fermer l'ancienne et continuer la nouvelle
|
||||
// Ici la nouvelle instance continue normalement
|
||||
} else {
|
||||
app.on("second-instance", () => {
|
||||
// La vieille instance se ferme
|
||||
if (mainWindow) {
|
||||
logger.info("New Instance Detected, closing the older...");
|
||||
mainWindow.destroy();
|
||||
mainWindow = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Création fenêtre principale
|
||||
async function createMainWindow() {
|
||||
if (mainWindow) {
|
||||
logger.warn("La fenetre existe deja, pas de nouvelle creation");
|
||||
logger.warn("Window already exists, no new creation!");
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
title: "Freedom Loader",
|
||||
title: `Freedom Loader ${config.version}`,
|
||||
width: 750,
|
||||
height: 800,
|
||||
minWidth: 750,
|
||||
@@ -38,42 +45,112 @@ async function createWindow() {
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
// titleBarStyle: 'hidden',
|
||||
});
|
||||
|
||||
try {
|
||||
await mainWindow.loadURL("http://localhost:8080");
|
||||
logger.info("Fenetre chargee");
|
||||
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
|
||||
logger.info("Window Loaded");
|
||||
} catch (err) {
|
||||
logger.error("Erreur chargement fenetre:", err);
|
||||
logger.error("Window Loading Error:", err);
|
||||
}
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
logger.info("Fenetre principale fermee");
|
||||
logger.info("Main Window Closed");
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
// IPC
|
||||
ipcMain.handle("select-download-folder", async () => {
|
||||
try {
|
||||
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
logger.info(`Dossier sélectionné : ${result.filePaths[0]}`);
|
||||
return result.filePaths[0];
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
logger.error(`Error when creating Output Folder : ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("get-default-download-path", () => defaultDownloadPath);
|
||||
|
||||
ipcMain.on("set-progress", (event, percent) => {
|
||||
if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1
|
||||
});
|
||||
|
||||
// Menu
|
||||
function setupMenu() {
|
||||
const menuTemplate = [
|
||||
{
|
||||
label: "Logs",
|
||||
submenu: [
|
||||
{
|
||||
label: "Open Logs",
|
||||
click: () => shell.openPath(logsFolderPath),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Website",
|
||||
submenu: [
|
||||
{
|
||||
label: "Go to Website",
|
||||
click: () => shell.openExternal("https://masteracnolo.github.io/FreedomLoader/"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Documentation",
|
||||
submenu: [
|
||||
{
|
||||
label: "Go to Wiki",
|
||||
click: () => shell.openExternal("https://masteracnolo.github.io/FreedomLoader/pages/wiki.html"),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
const defaultMenu = Menu.getApplicationMenu();
|
||||
const mergedTemplate = defaultMenu
|
||||
? [...defaultMenu.items.map(item => item), ...menuTemplate]
|
||||
: menuTemplate;
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(mergedTemplate));
|
||||
}
|
||||
|
||||
// App ready
|
||||
app.whenReady().then(async () => {
|
||||
logSessionStart();
|
||||
logger.info("App prete, demarrage du serveur Express...");
|
||||
const expressServer = require("./server/server.js");
|
||||
logger.info("App Ready, Server Express starting...");
|
||||
|
||||
const serverPath = path.join(__dirname, "server", "server.js");
|
||||
const expressServer = require(serverPath);
|
||||
|
||||
try {
|
||||
await expressServer.startServer();
|
||||
logger.info("Serveur Express demarre");
|
||||
await createWindow();
|
||||
} catch (error) {
|
||||
logger.error("Erreur serveur ou fenetre :", error);
|
||||
logger.info("Express Server Started");
|
||||
|
||||
const { startRPC } = require("./server/discordRPC");
|
||||
startRPC();
|
||||
|
||||
await createMainWindow();
|
||||
AutoUpdater(mainWindow);
|
||||
setupMenu();
|
||||
} catch (err) {
|
||||
logger.error("Window or Server error :", err);
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
logger.info("Toutes fenetres fermees, quitte l'app");
|
||||
logger.info("All Window Closed, shuting down app");
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
|
||||
app.on("before-quit", () => {
|
||||
logSessionEnd();
|
||||
});
|
||||
app.on("before-quit", () => logSessionEnd());
|
||||
|
||||
3932
package-lock.json
generated
71
package.json
@@ -1,56 +1,93 @@
|
||||
{
|
||||
"name": "freedom-loader",
|
||||
"productName": "Freedom Loader",
|
||||
"version": "1.0.1",
|
||||
"version": "1.3.0",
|
||||
"author": "MasterAcnolo",
|
||||
"description": "Freedom Loader, parce que nous le valons bien",
|
||||
"description": "Freedom Loader",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"start": "electron --trace-warnings .",
|
||||
"dist": "electron-builder"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"debug": "^4.4.1",
|
||||
"discord-rpc": "^4.0.1",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-updater": "^6.6.2",
|
||||
"express": "^5.1.0",
|
||||
"webidl-conversions": "^8.0.0",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.8.1",
|
||||
"@electron-forge/maker-deb": "^7.8.1",
|
||||
"@electron-forge/maker-rpm": "^7.8.1",
|
||||
"@electron-forge/maker-squirrel": "^7.8.1",
|
||||
"@electron-forge/maker-zip": "^7.8.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
|
||||
"@electron-forge/plugin-fuses": "^7.8.1",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"electron": "^37.1.0",
|
||||
"electron-builder": "^26.0.12"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.masteracnolo.freedomloader",
|
||||
"productName": "Freedom Loader",
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"owner": "MasterAcnolo",
|
||||
"repo": "Freedom-Loader"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"**/*",
|
||||
"node_modules/**/*"
|
||||
"node_modules/**/*",
|
||||
"!logs/*.json",
|
||||
"server/**/*",
|
||||
"server/node_modules/**/*"
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "build"
|
||||
},
|
||||
"asar": false,
|
||||
"asar": true,
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "build/confirm-icon.png",
|
||||
"to": "confirm-icon.png"
|
||||
},
|
||||
{
|
||||
"from": "build/app-icon.ico",
|
||||
"to": "app-icon.ico"
|
||||
},
|
||||
{
|
||||
"from": "build/error.png",
|
||||
"to": "error.png"
|
||||
},
|
||||
{
|
||||
"from": "ressources/yt-dlp.exe",
|
||||
"to": "yt-dlp.exe"
|
||||
},
|
||||
{
|
||||
"from": "ressources/ffmpeg.exe",
|
||||
"to": "ffmpeg.exe"
|
||||
},
|
||||
{
|
||||
"from": "ressources/ffprobe.exe",
|
||||
"to": "ffprobe.exe"
|
||||
},
|
||||
{
|
||||
"from": "ressources/deno.exe",
|
||||
"to": "deno.exe"
|
||||
},
|
||||
{
|
||||
"from": "server",
|
||||
"to": "server"
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "build/installer-icon.ico"
|
||||
"icon": "build/app-icon.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": true,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"installerIcon": "build/installer-icon.ico",
|
||||
"installerIcon": "build/app-icon.ico",
|
||||
"uninstallerIcon": "build/uninstaller-icon.ico",
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
|
||||
7
preload.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
|
||||
selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
|
||||
setProgress: (percent) => ipcRenderer.send("set-progress", percent)
|
||||
});
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 97 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" transform="scale(64)" fill="#1B1F23"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 968 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zm2 226.3c37.1 22.4 62 63.1 62 109.7s-24.9 87.3-62 109.7c-7.6 4.6-17.4 2.1-22-5.4s-2.1-17.4 5.4-22C269.4 401.5 288 370.9 288 336s-18.6-65.5-46.5-82.3c-7.6-4.6-10-14.4-5.4-22s14.4-10 22-5.4zm-91.9 30.9c6 2.5 9.9 8.3 9.9 14.8l0 128c0 6.5-3.9 12.3-9.9 14.8s-12.9 1.1-17.4-3.5L113.4 376 80 376c-8.8 0-16-7.2-16-16l0-48c0-8.8 7.2-16 16-16l33.4 0 35.3-35.3c4.6-4.6 11.5-5.9 17.4-3.5zm51 34.9c6.6-5.9 16.7-5.3 22.6 1.3C249.8 304.6 256 319.6 256 336s-6.2 31.4-16.3 42.7c-5.9 6.6-16 7.1-22.6 1.3s-7.1-16-1.3-22.6c5.1-5.7 8.1-13.1 8.1-21.3s-3.1-15.7-8.1-21.3c-5.9-6.6-5.3-16.7 1.3-22.6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 955 B |
BIN
public/assets/images/Chirac.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
public/assets/images/Drift.jpg
Normal file
|
After Width: | Height: | Size: 967 KiB |
BIN
public/assets/images/NF.jpg
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
public/assets/images/Songbird.webp
Normal file
|
After Width: | Height: | Size: 620 KiB |
BIN
public/assets/images/Spicy.png
Normal file
|
After Width: | Height: | Size: 5.3 MiB |
BIN
public/assets/logo/Freedom Loader White.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/assets/logo/Freedom Loader.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/assets/logo/FreedomLoader2.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
@@ -9,66 +9,83 @@
|
||||
font-src https://fonts.gstatic.com;
|
||||
img-src 'self' data: https://i.ytimg.com;
|
||||
">
|
||||
<title>Freedom Loader</title>
|
||||
<title></title>
|
||||
|
||||
<!--CSS-->
|
||||
<link rel="stylesheet" href="styles/styles.css">
|
||||
|
||||
<!--Icons-->
|
||||
<link rel="icon" href="assets/icons/file-audio-solid.svg" type="image/svg+xml">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="theme-switcher">
|
||||
<label for="themeSelect">Thème :</label>
|
||||
<select id="themeSelect" aria-label="Choisir le thème"></select>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<header>
|
||||
<h1> Freedom Loader </h1>
|
||||
<p> Because why not? </p>
|
||||
</header>
|
||||
<header>
|
||||
<h1 id="title"> Freedom Loader </h1>
|
||||
<p id="subtitle"> Because why not? </p>
|
||||
</header>
|
||||
|
||||
<form action="/download" method="POST" id="downloadForm">
|
||||
<input name="url" placeholder="URL" id="UrlInput">
|
||||
<form action="/download" method="POST" id="downloadForm">
|
||||
<input name="url" placeholder="URL" id="UrlInput">
|
||||
|
||||
<label class="checkbox-inline">
|
||||
<label class="checkbox-inline">
|
||||
|
||||
<label class="checkbox-mp3">
|
||||
<input type="checkbox" name="audioOnly" value="1">
|
||||
<div class="checkmark"></div>
|
||||
</label>
|
||||
<!-- <input type="checkbox" name="audioOnly" value="1"> -->
|
||||
Audio seulement (MP3)
|
||||
<label class="checkbox-mp3">
|
||||
<input type="checkbox" name="audioOnly" value="1">
|
||||
<div class="checkmark"></div>
|
||||
</label>
|
||||
<!-- <input type="checkbox" name="audioOnly" value="1"> Audio seulement (MP3) -->
|
||||
<select name="quality">
|
||||
<option value="best">Meilleure qualité</option>
|
||||
<option value="medium">Qualité moyenne</option>
|
||||
<option value="worst">Qualité minimale</option>
|
||||
<option value="1080">1080p</option>
|
||||
<option value="720">720p</option>
|
||||
<option value="480">480p</option>
|
||||
</select>
|
||||
|
||||
<button type="submit">Télécharger</button>
|
||||
</form>
|
||||
|
||||
<div id="downloadStatus"></div>
|
||||
|
||||
<!--Il faut utiliser un formulaire, parce que on doit envoyer a un serveur
|
||||
les données à traiter. Pour cela on définit une route vers /download, ca va
|
||||
être l'espèce de "canal" attribué a ça. Ca va faire que si jamais on veut ensuite recup
|
||||
les infos du form, il faut aller sur ce canal précis (/download). La méthode POST dit
|
||||
que l'on envoie des données.-->
|
||||
|
||||
|
||||
Audio Only (MP3)
|
||||
</label>
|
||||
<select name="quality">
|
||||
<option value="best">Best Quality</option>
|
||||
<option value="medium">Medium Quality</option>
|
||||
<option value="worst">Lowest Quality</option>
|
||||
<option value="1080">1080p</option>
|
||||
<option value="720">720p</option>
|
||||
<option value="480">480p</option>
|
||||
</select>
|
||||
|
||||
<button type="submit" onclick="startProgress()">Download</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="download-path">
|
||||
Will be save on <span id="savePath"></span>
|
||||
<button id="changePath">Edit</button>
|
||||
</div>
|
||||
|
||||
<div id="downloadProgressWrapper">
|
||||
<div id="downloadProgress"></div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px;">
|
||||
<div id="downloadProgressText"></div>
|
||||
<div id="downloadSpeedText"></div>
|
||||
</div>
|
||||
|
||||
<div id="downloadStatus"></div>
|
||||
|
||||
<div class="infos-container">
|
||||
<div id="videoInfo" style="margin-top: 20px;"></div>
|
||||
|
||||
<div id="loaderBox" class="loader-box" style="display:none;">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<span>Fetching info...</span>
|
||||
</div>
|
||||
|
||||
<div id="videoInfo" style="margin-top: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="script/custompath.js"></script>
|
||||
<script src="script/downloadstatus.js"></script>
|
||||
<script src="script/fetchinfo.js"></script>
|
||||
<script src="script/progressBar.js"></script>
|
||||
<script src="script/customthemes.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
41
public/script/custompath.js
Normal file
@@ -0,0 +1,41 @@
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
const savePathElem = document.getElementById("savePath");
|
||||
|
||||
// Essayer de charger depuis le localStorage
|
||||
let savedPath = localStorage.getItem("customDownloadPath");
|
||||
|
||||
// Sinon demander le chemin par défaut à l'API Electron
|
||||
if (!savedPath) {
|
||||
savedPath = await window.electronAPI.getDefaultDownloadPath();
|
||||
}
|
||||
|
||||
// Afficher le chemin
|
||||
if (savePathElem) {
|
||||
savePathElem.textContent = savedPath;
|
||||
}
|
||||
|
||||
// Créer l'input caché s'il n'existe pas déjà
|
||||
let hidden = document.getElementById("savePathInput");
|
||||
if (!hidden) {
|
||||
hidden = document.createElement("input");
|
||||
hidden.type = "hidden";
|
||||
hidden.name = "savePath";
|
||||
hidden.id = "savePathInput";
|
||||
document.getElementById("downloadForm").appendChild(hidden);
|
||||
}
|
||||
hidden.value = savedPath;
|
||||
|
||||
// Gestion du bouton de modification
|
||||
document.getElementById("changePath").addEventListener("click", async () => {
|
||||
const selectedPath = await window.electronAPI.selectDownloadFolder();
|
||||
if (selectedPath) {
|
||||
// Met à jour l'affichage
|
||||
savePathElem.textContent = selectedPath;
|
||||
hidden.value = selectedPath;
|
||||
|
||||
// Et le stocke en localStorage pour la prochaine fois
|
||||
localStorage.setItem("customDownloadPath", selectedPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
63
public/script/customthemes.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const themes = {
|
||||
default: { label: "Default", subtitle: "Because why not?" },
|
||||
dark: { label: "Sombre", subtitle: "Darkness is my ally" },
|
||||
light: { label: "Clair", subtitle: "Qui aime ce thème ?" },
|
||||
neon: { label: "Néon", subtitle: "How was your day ?"},
|
||||
nf: { label: "NF", subtitle: "You call it music, i call it my Therapist" },
|
||||
songbird: { label: "Songbird", subtitle: "From Her to Eternity" },
|
||||
drift: { label: "Drift", subtitle: "Si la route t'appelle, contre appel" },
|
||||
fanatic: { label: "Fanatic", subtitle: "Always Fnatic !" },
|
||||
chirac: { label: "Chirac", subtitle: "J'aime les pommes" },
|
||||
spicy: { label: "Spicy", subtitle: "The Spiciest One" },
|
||||
};
|
||||
|
||||
const themeSelect = document.getElementById("themeSelect");
|
||||
|
||||
function populateThemeSelect() {
|
||||
for (const [themeKey, themeInfo] of Object.entries(themes)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = themeKey;
|
||||
option.textContent = themeInfo.label;
|
||||
themeSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
function applyTheme(themeKey) {
|
||||
document.body.classList.remove(...Object.keys(themes));
|
||||
document.body.classList.add(themeKey);
|
||||
|
||||
const subtitleElement = document.getElementById("subtitle");
|
||||
if (subtitleElement && themes[themeKey]) {
|
||||
subtitleElement.textContent = themes[themeKey].subtitle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function saveTheme(themeKey) {
|
||||
localStorage.setItem("selectedTheme", themeKey);
|
||||
}
|
||||
|
||||
function loadTheme() {
|
||||
const savedTheme = localStorage.getItem("selectedTheme");
|
||||
|
||||
if (savedTheme && themes[savedTheme]) {
|
||||
applyTheme(savedTheme);
|
||||
themeSelect.value = savedTheme;
|
||||
} else {
|
||||
applyTheme("dark"); // thème par défaut
|
||||
themeSelect.value = "dark";
|
||||
}
|
||||
}
|
||||
|
||||
// Quand l'utilisateur change le thème depuis le select
|
||||
themeSelect.addEventListener("change", (event) => {
|
||||
const selectedTheme = event.target.value;
|
||||
if (themes[selectedTheme]) {
|
||||
applyTheme(selectedTheme);
|
||||
saveTheme(selectedTheme);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialisation
|
||||
populateThemeSelect();
|
||||
loadTheme();
|
||||
@@ -1,71 +1,35 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
|
||||
Freedom Loader is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
|
||||
Freedom Loader is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Récupération du formulaire de téléchargement
|
||||
const form = document.getElementById("downloadForm");
|
||||
|
||||
// Div où on affiche l'état de progression (en cours, erreur, succès)
|
||||
const statusDiv = document.getElementById("downloadStatus");
|
||||
|
||||
// Récupération du bouton de validation pour pouvoir le désactiver pendant la requête
|
||||
const button = form.querySelector("button");
|
||||
|
||||
// On intercepte la soumission du formulaire
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault(); // empêche le rechargement de la page par le navigateur
|
||||
e.preventDefault();
|
||||
button.disabled = true; // Empêche les clics multiples
|
||||
statusDiv.textContent = "Download in progress...";
|
||||
|
||||
// On désactive le bouton pour éviter les clics multiples
|
||||
button.disabled = true;
|
||||
|
||||
// Affiche un message d'attente à l'utilisateur
|
||||
statusDiv.textContent = "Téléchargement en cours...";
|
||||
|
||||
// Prépare les données du formulaire sous forme URL-encoded
|
||||
const formData = new FormData(form);
|
||||
const params = new URLSearchParams(formData);
|
||||
|
||||
try {
|
||||
// Envoi de la requête POST vers /download avec les données du formulaire
|
||||
const res = await fetch("/download", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: params,
|
||||
});
|
||||
|
||||
// Si la réponse n'est pas un succès (code 4xx ou 5xx)
|
||||
if (!res.ok) {
|
||||
statusDiv.textContent = "❌ Erreur pendant le téléchargement.";
|
||||
statusDiv.textContent = res;
|
||||
return;
|
||||
}
|
||||
|
||||
// On récupère le texte de la réponse du serveur
|
||||
const text = await res.text();
|
||||
|
||||
// Affiche le message de fin de téléchargement (par exemple « Téléchargement terminé »)
|
||||
statusDiv.textContent = text;
|
||||
|
||||
} catch {
|
||||
// Gestion d'erreur en cas de problème réseau ou autre
|
||||
statusDiv.textContent = "❌ Une erreur s’est produite.";
|
||||
statusDiv.textContent = "❌ An Error has Occured.";
|
||||
} finally {
|
||||
// Réactive le bouton pour autoriser d'autres soumissions
|
||||
button.disabled = false;
|
||||
|
||||
// Efface le message de status après 5 secondes pour garder l'UI propre
|
||||
setTimeout(() => {
|
||||
statusDiv.textContent = "";
|
||||
}, 5000);
|
||||
|
||||
@@ -1,114 +1,180 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr || dateStr.length !== 8) return "Inconnue";
|
||||
return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`;
|
||||
}
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
function formatSize(bytes) {
|
||||
return bytes ? (bytes / (1024*1024)).toFixed(2) + " Mo" : "Inconnue";
|
||||
}
|
||||
|
||||
Freedom Loader is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
async function fetchVideoInfo(url) {
|
||||
try {
|
||||
const res = await fetch("/info", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ url }),
|
||||
});
|
||||
|
||||
Freedom Loader is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
if (!res.ok) return { error: `An Error occured when fetching info` };
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const data = await res.json();
|
||||
if (!data) return { error: "Data is Missing" };
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
return { error: "Network or JSON Issue" };
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const urlInput = document.getElementById("UrlInput");
|
||||
const infoDiv = document.getElementById("videoInfo");
|
||||
const loaderBox = document.getElementById("loaderBox");
|
||||
|
||||
let lastFetchedUrl = "";
|
||||
const DEBUG = true;
|
||||
|
||||
urlInput.addEventListener("input", async () => {
|
||||
const url = urlInput.value.trim();
|
||||
if (DEBUG) console.log("[DEBUG] Input détecté :", url);
|
||||
|
||||
if (!url || url.length < 5) {
|
||||
if (DEBUG) console.log("[DEBUG] URL vide ou trop courte, reset affichage.");
|
||||
// Si champ vide -> reset total
|
||||
if (!url || url.length < 2) {
|
||||
infoDiv.innerHTML = "";
|
||||
infoDiv.classList.remove("visible");
|
||||
infoDiv.classList.remove("visible", "playlist-mode");
|
||||
lastFetchedUrl = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (url === lastFetchedUrl) {
|
||||
if (DEBUG) console.log("[DEBUG] Même URL que précédemment, pas de fetch.");
|
||||
if (url === lastFetchedUrl) return;
|
||||
lastFetchedUrl = url;
|
||||
|
||||
loaderBox.style.display = "flex";
|
||||
const data = await fetchVideoInfo(url);
|
||||
loaderBox.style.display = "none";
|
||||
|
||||
// Gestion des erreurs
|
||||
if (data.error) {
|
||||
infoDiv.innerHTML = `
|
||||
<div style="
|
||||
padding:7px;
|
||||
background:var(----infos-box-color);
|
||||
">
|
||||
<strong>${data.error}</strong>
|
||||
</div>
|
||||
`;
|
||||
infoDiv.classList.add("visible");
|
||||
infoDiv.classList.remove("playlist-mode");
|
||||
loaderBox.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
lastFetchedUrl = url;
|
||||
// ---------- PLAYLIST ----------
|
||||
if (data.type === "playlist") {
|
||||
infoDiv.classList.add("playlist-mode");
|
||||
infoDiv.innerHTML = `
|
||||
<h3 style="color:var(--video-info-heading-color);"><strong>Playlist Detected: ${data.title}</strong></h3>
|
||||
<h3 style="color:var(--video-info-heading-color);"><strong>Video Count: ${data.count}</strong></h3>
|
||||
<p><strong>Channel :</strong> ${data.channel || "Unknown"}</p>
|
||||
<div id="playlistVideos"></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
if (DEBUG) console.log("[DEBUG] Envoi requête POST vers /info avec URL:", url);
|
||||
const res = await fetch("/info", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ url }),
|
||||
const listDiv = document.getElementById("playlistVideos");
|
||||
|
||||
data.videos.forEach(v => {
|
||||
const durationStr = v.duration
|
||||
? `${Math.floor(v.duration / 60)}m ${(v.duration % 60).toString().padStart(2,"0")}s` : "Inconnue";
|
||||
|
||||
const videoUrl = v.id ? `https://www.youtube.com/watch?v=${v.id}` : v.url;
|
||||
|
||||
listDiv.innerHTML += `
|
||||
<div style="margin-bottom:12px;">
|
||||
<img src="${v.thumbnail}" width="160" alt="Thumbnail">
|
||||
<p><strong>${v.title}</strong></p>
|
||||
<p>Duration : ${durationStr}</p>
|
||||
<p><a href="${videoUrl}" target="_blank">URL</a>
|
||||
<button class="copy-btn" data-url="${videoUrl}">📋</button>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
if (DEBUG) console.log("[DEBUG] Réponse status:", res.status);
|
||||
// Gestion du bouton copier
|
||||
listDiv.addEventListener("click", (event) => {
|
||||
if (event.target.classList.contains("copy-btn")) {
|
||||
const btn = event.target;
|
||||
if (btn.disabled) return;
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(`[ERROR] Réponse HTTP ${res.status}: ${res.statusText}`);
|
||||
infoDiv.innerHTML = "❌ Impossible de récupérer les infos (erreur serveur).";
|
||||
infoDiv.classList.remove("visible");
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
const url = btn.dataset.url;
|
||||
navigator.clipboard.writeText(url)
|
||||
.then(() => {
|
||||
const original = btn.textContent;
|
||||
|
||||
const data = await res.json();
|
||||
if (DEBUG) console.log("[DEBUG] Données reçues:", data);
|
||||
btn.style.opacity = 0;
|
||||
btn.style.transform = "scale(0.7)";
|
||||
|
||||
// Vérification minimaliste
|
||||
if (!data || !data.title || !data.thumbnail) {
|
||||
console.warn("[WARNING] Données incomplètes", data);
|
||||
infoDiv.innerHTML = "❌ Données incomplètes reçues.";
|
||||
infoDiv.classList.remove("visible");
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
btn.textContent = "✅";
|
||||
btn.style.opacity = 1;
|
||||
btn.style.transform = "scale(1)";
|
||||
|
||||
const minutes = Math.floor(data.duration / 60);
|
||||
const seconds = data.duration % 60;
|
||||
const durationStr = `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
|
||||
setTimeout(() => {
|
||||
btn.style.opacity = 0;
|
||||
btn.style.transform = "scale(0.7)";
|
||||
|
||||
const date = data.upload_date || "";
|
||||
const readableDate = date
|
||||
? `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}`
|
||||
: "Inconnue";
|
||||
setTimeout(() => {
|
||||
btn.textContent = original;
|
||||
btn.style.opacity = 1;
|
||||
btn.style.transform = "scale(1)";
|
||||
btn.disabled = false;
|
||||
}, 300);
|
||||
|
||||
const sizeMB = data.filesize_approx
|
||||
? (data.filesize_approx / (1024 * 1024)).toFixed(2) + " Mo"
|
||||
: "Inconnue";
|
||||
}, 1000);
|
||||
|
||||
const categories = data.categories
|
||||
? data.categories.join(", ")
|
||||
: "Non spécifiées";
|
||||
}, 300);
|
||||
})
|
||||
.catch(() => {
|
||||
const original = btn.textContent;
|
||||
btn.textContent = "❌";
|
||||
setTimeout(() => {
|
||||
btn.textContent = original;
|
||||
btn.disabled = false;
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
infoDiv.innerHTML = `
|
||||
<h3>${data.title}</h3>
|
||||
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
||||
<ul>
|
||||
<li><strong>Durée :</strong> ${durationStr}</li>
|
||||
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
|
||||
<li><strong>Date d’upload :</strong> ${readableDate}</li>
|
||||
<li><strong>Vues :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
|
||||
<li><strong>Likes :</strong> ${data.like_count?.toLocaleString() || "?"}</li>
|
||||
<li><strong>URL :</strong> <a href="${data.webpage_url}" target="_blank" rel="noopener noreferrer">${data.webpage_url}</a></li>
|
||||
<li><strong>Channel :</strong> <a href="${data.channel_url}" target="_blank" rel="noopener noreferrer">${data.channel_url}</a></li>
|
||||
<li><strong>Taille estimée :</strong> ${sizeMB}</li>
|
||||
<li><strong>Catégories :</strong> ${categories}</li>
|
||||
</ul>
|
||||
`;
|
||||
infoDiv.classList.add("visible");
|
||||
|
||||
if (DEBUG) console.log("[DEBUG] Info affichée avec succès.");
|
||||
} catch (e) {
|
||||
console.error("[CRITICAL] Erreur lors de la récupération:", e);
|
||||
infoDiv.innerHTML = "❌ Erreur réseau ou JSON.";
|
||||
infoDiv.classList.remove("visible");
|
||||
return;
|
||||
}
|
||||
|
||||
infoDiv.classList.remove("playlist-mode");
|
||||
|
||||
// ---------- VIDEO NORMALE ----------
|
||||
const durationStr = data.duration
|
||||
? `${Math.floor(data.duration/60)}m ${(data.duration%60).toString().padStart(2,"0")}s`
|
||||
: "Unknown";
|
||||
|
||||
const sizeStr = formatSize(data.filesize_approx);
|
||||
const readableDate = formatDate(data.upload_date);
|
||||
const categories = data.categories?.join(", ") || "Non spécifiées";
|
||||
|
||||
infoDiv.innerHTML = `
|
||||
<h3>${data.title}</h3>
|
||||
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
||||
<ul>
|
||||
<li><strong>Duration :</strong> ${durationStr}</li>
|
||||
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
|
||||
<li><strong>Upload Date :</strong> ${readableDate}</li>
|
||||
<li><strong>Views :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
|
||||
<li><strong>Likes :</strong> ${data.like_count?.toLocaleString() || "?"}</li>
|
||||
<li><strong>URL :</strong> <a href="${data.webpage_url}" target="_blank">${data.webpage_url}</a></li>
|
||||
<li><strong>Channel :</strong> <a href="${data.channel_url}" target="_blank">${data.channel_url}</a></li>
|
||||
<li><strong>Estimed Size :</strong> ${sizeStr}</li>
|
||||
<li><strong>Category :</strong> ${categories}</li>
|
||||
</ul>
|
||||
`;
|
||||
|
||||
infoDiv.classList.add("visible");
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
63
public/script/progressBar.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||
const progressBar = document.getElementById("downloadProgress");
|
||||
const progressBarText = document.getElementById("downloadProgressText")
|
||||
|
||||
const speedElement = document.getElementById("downloadSpeedText");
|
||||
const speedEvt = new EventSource("/download/speed");
|
||||
|
||||
function startProgress() {
|
||||
progressWrapper.style.display = "block";
|
||||
progressBar.style.width = "0%";
|
||||
progressBarText.style.display = "block";
|
||||
progressBarText.innerHTML = "0%";
|
||||
|
||||
speedElement.style.display = "block";
|
||||
speedElement.innerHTML = "0 Mib/s";
|
||||
}
|
||||
|
||||
function updateProgress(percent) {
|
||||
progressBar.style.width = `${percent}%`;
|
||||
progressBarText.innerHTML = `${percent}%`;
|
||||
}
|
||||
|
||||
function resetProgress() {
|
||||
progressBar.style.width = "0%";
|
||||
progressBarText.textContent = "";
|
||||
progressBarText.style.display = "none";
|
||||
progressWrapper.style.display = "none";
|
||||
|
||||
speedElement.textContent = "0 Mib/s";
|
||||
speedElement.style.display = "none";
|
||||
}
|
||||
|
||||
// Connexion SSE
|
||||
const evtSource = new EventSource("/download/progress");
|
||||
evtSource.onmessage = e => {
|
||||
if (e.data === "reset") {
|
||||
startProgress();
|
||||
window.electronAPI.setProgress(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.data === "done") {
|
||||
resetProgress();
|
||||
window.electronAPI.setProgress(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
const percent = parseFloat(e.data);
|
||||
|
||||
if (!isNaN(percent)) {
|
||||
updateProgress(percent);
|
||||
window.electronAPI.setProgress(percent); // update barre des tâches
|
||||
if (percent >= 100) setTimeout(() => {
|
||||
resetProgress();
|
||||
window.electronAPI.setProgress(-1); // retire la barre
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
speedEvt.onmessage = e => {
|
||||
speedElement.style.display = "block";
|
||||
speedElement.textContent = e.data; // ex: "5.2MiB/s"
|
||||
};
|
||||
@@ -15,15 +15,15 @@
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
--clr: #3B82F6 ;
|
||||
--clr: var(--checkbox-bg-checked);
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 1.3em;
|
||||
width: 1.3em;
|
||||
background-color: #ccc;
|
||||
border-radius: 50%;
|
||||
transition: 300ms;
|
||||
background-color: var(--checkbox-bg-default);
|
||||
border-radius: .5rem;
|
||||
transition: background-color 300ms, border-radius 300ms;
|
||||
}
|
||||
|
||||
.checkbox-mp3 input:checked ~ .checkmark {
|
||||
@@ -47,7 +47,7 @@
|
||||
top: 0.25em;
|
||||
width: 0.25em;
|
||||
height: 0.5em;
|
||||
border: solid #E0E0E2;
|
||||
border: solid var(--checkbox-checkmark-border-color);
|
||||
border-width: 0 0.15em 0.15em 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
42
public/styles/components/editpathbutton.css
Normal file
@@ -0,0 +1,42 @@
|
||||
.download-path {
|
||||
margin-top: 1em;
|
||||
font-size: 1rem;
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
|
||||
.download-path #savePath:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.download-path #savePath {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
color: var(--form-input-text-color);
|
||||
padding: 0.3em 0.6em;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
border: 1px dashed var(--form-input-border-color);
|
||||
word-break: break-all;
|
||||
display: inline-block;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
|
||||
.download-path button#changePath {
|
||||
margin-left: 0.5em;
|
||||
background-color: var(--form-button-bg-color);
|
||||
color: var(--form-button-text-color);
|
||||
border: none;
|
||||
padding: 0.3em 0.8em;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 550;
|
||||
}
|
||||
|
||||
.download-path button#changePath:hover {
|
||||
background-color: var(--form-button-bg-hover-color);
|
||||
}
|
||||
34
public/styles/components/loader.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.loader-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 20px;
|
||||
background: var(--form-bg-color);
|
||||
border: 2px solid var(--mp3-text-color);
|
||||
border-radius: 8px;
|
||||
color: var(--mp3-text-color);
|
||||
font-weight: bold;
|
||||
animation: pulse 1.2s infinite;
|
||||
}
|
||||
|
||||
.loader-box div {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: var(--form-button-bg-color);
|
||||
border-radius: 3px;
|
||||
animation: bounce 1.2s infinite;
|
||||
}
|
||||
|
||||
.loader-box div:nth-child(1) { animation-delay: 0s; }
|
||||
.loader-box div:nth-child(2) { animation-delay: 0.2s; }
|
||||
.loader-box div:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% { transform: scale(0); }
|
||||
40% { transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 0 5px var(--form-button-bg-color); }
|
||||
50% { box-shadow: 0 0 20px var(--form-button-bg-color); }
|
||||
}
|
||||
41
public/styles/components/progressBar.css
Normal file
@@ -0,0 +1,41 @@
|
||||
/* Wrapper de la barre de téléchargement */
|
||||
#downloadProgressWrapper {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 12px;
|
||||
background-color: #444;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin: 10px auto;
|
||||
display: none;
|
||||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
/* Barre de progression */
|
||||
#downloadProgress {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background-color: var(--video-info-link-color, #4caf50);
|
||||
border-radius: 6px 0 0 6px;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
#downloadProgressText, #downloadSpeedText {
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
color: #fff;
|
||||
content: "";
|
||||
|
||||
}
|
||||
|
||||
/* Responsive pour mobile */
|
||||
@media (max-width: 480px) {
|
||||
#downloadProgressWrapper {
|
||||
max-width: 90%;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
#downloadProgressText, #downloadSpeedText {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
38
public/styles/components/themebutton.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.theme-switcher {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: #242424;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-family: sans-serif;
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.theme-switcher label {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.theme-switcher select {
|
||||
background: #fff;
|
||||
border: 1px solid #242424;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-switcher select:hover,
|
||||
.theme-switcher select:focus {
|
||||
background: #474747;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
@@ -3,10 +3,23 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* height: 10000px; */
|
||||
padding: 1em;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#downloadStatus {
|
||||
margin-top: 1em;
|
||||
min-height: 1.5em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: var(--download-status-color);
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#downloadStatus {
|
||||
|
||||
margin-top: 1em;
|
||||
min-height: 1.5em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
form {
|
||||
background-color: #fff;
|
||||
background-color: var(--form-bg-color);
|
||||
padding: 2em;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
@@ -13,8 +13,10 @@ form input[type="text"],
|
||||
form input[type="url"],
|
||||
form select {
|
||||
padding: 0.75em;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid var(--form-input-border-color);
|
||||
border-radius: 8px;
|
||||
background-color: var(--form-input-bg-color);
|
||||
color: var(--form-input-text-color);
|
||||
}
|
||||
|
||||
form input[type="checkbox"] {
|
||||
@@ -22,8 +24,8 @@ form input[type="checkbox"] {
|
||||
}
|
||||
|
||||
form button {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
background-color: var(--form-button-bg-color);
|
||||
color: var(--form-button-text-color);
|
||||
border: none;
|
||||
padding: 0.75em;
|
||||
border-radius: 8px;
|
||||
@@ -32,42 +34,43 @@ form button {
|
||||
}
|
||||
|
||||
form button:hover {
|
||||
background-color: #0056b3;
|
||||
background-color: var(--form-button-bg-hover-color);
|
||||
}
|
||||
|
||||
form#downloadForm {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkbox-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
gap: 0.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
gap: 0.5em;
|
||||
color: var(--form-input-text-color);
|
||||
}
|
||||
|
||||
#UrlInput {
|
||||
padding: 0.75em 1em;
|
||||
border-color: #ccc;
|
||||
box-shadow: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
padding: 0.75em 1em;
|
||||
border-color: var(--form-input-border-color);
|
||||
box-shadow: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
background-color: var(--form-input-bg-color);
|
||||
color: var(--form-input-text-color);
|
||||
}
|
||||
|
||||
#UrlInput:focus {
|
||||
border-color: #0056b3;
|
||||
box-shadow: 0 0 5px rgba(0,123,255,0.4);
|
||||
outline: none;
|
||||
background-color: #fff;
|
||||
border-color: var(--form-input-border-focus-color);
|
||||
box-shadow: 0 0 5px rgba(0,123,255,0.4);
|
||||
outline: none;
|
||||
background-color: var(--form-input-bg-color);
|
||||
}
|
||||
|
||||
#UrlInput::placeholder {
|
||||
color: #aaa;
|
||||
color: var(--form-input-placeholder-color);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
background-color: var(--infos-box-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||||
color: #222;
|
||||
color: var(--video-info-text-color);
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
opacity: 0;
|
||||
@@ -21,13 +21,12 @@
|
||||
#videoInfo.visible {
|
||||
opacity: 1;
|
||||
max-height: 1500px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#videoInfo h3 {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 0.6em;
|
||||
color: #007bff;
|
||||
color: var(--video-info-heading-color);
|
||||
font-weight: 700;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -51,15 +50,15 @@
|
||||
#videoInfo ul li {
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 500;
|
||||
color: #444;
|
||||
color: var(--video-info-list-color);
|
||||
}
|
||||
|
||||
#videoInfo ul li strong {
|
||||
color: #000;
|
||||
color: var(--video-info-list-strong-color);
|
||||
}
|
||||
|
||||
#videoInfo ul li a {
|
||||
color: #007bff;
|
||||
color: var(--video-info-link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -68,6 +67,81 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* --- Playlist Mode --- */
|
||||
#videoInfo.playlist-mode {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
/* Style du bloc "Playlist détectée" */
|
||||
#videoInfo.playlist-mode > p {
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Layout liste des vidéos */
|
||||
#playlistVideos {
|
||||
margin-top: 1em;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
/* Un item de playlist */
|
||||
#playlistVideos > div {
|
||||
background: var(--playlist-background-color);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
#playlistVideos > div p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#videoInfo.playlist-mode {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
#playlistVideos > div:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
#playlistVideos img {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
all: unset;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
transition: transform 0.1s ease, color 0.1s ease, opacity 0.15s ease;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
#playlistVideos > div p a{
|
||||
text-decoration: none;
|
||||
color: var(--video-info-link-color);
|
||||
}
|
||||
|
||||
#playlistVideos > div p a:hover{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 480px) {
|
||||
#videoInfo {
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
/* Layout */
|
||||
@import url("layout/form.css");
|
||||
@import url("layout/container.css");
|
||||
@import url("layout/ctr-infos.css");
|
||||
@import url("layout/header.css");
|
||||
@import url("layout/videoinfo.css");
|
||||
|
||||
/* Components */
|
||||
@import url("components/checkbox.css");
|
||||
@import url("variables.css");
|
||||
@import url("components/themebutton.css");
|
||||
@import url("components/loader.css");
|
||||
|
||||
@import url("theme/themeimport.css");
|
||||
@import url("components/editpathbutton.css");
|
||||
@import url("components/progressBar.css");
|
||||
|
||||
@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');
|
||||
|
||||
#app {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s ease;
|
||||
}
|
||||
#app.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
html,body{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
scroll-behavior: smooth;
|
||||
/* outline: 2px solid red; */ /*<-- Utile pour le début ça */
|
||||
/*outline: 2px solid red; /*<-- Utile pour le debug ça */
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
@@ -21,10 +37,6 @@ html,body{
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: var(--background-color);
|
||||
/* background-image: linear-gradient(to left top, #838383, #a1a1a1, #bfbfbf, #dfdfdf, #ffffff);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed; */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--default-text-color);
|
||||
@@ -45,22 +57,11 @@ header p{
|
||||
color: var(--mp3-text-color);
|
||||
}
|
||||
|
||||
/* #videoInfo {
|
||||
margin-top: 100px;
|
||||
padding: 1em;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-radius: 8px;
|
||||
color: rgb(0, 0, 0);
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
||||
transition: opacity 0.5s ease, max-height 0.5s ease;
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
body,
|
||||
.container,
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
label {
|
||||
transition: background-color 0.2s ease, color 0.5s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
#videoInfo.visible {
|
||||
opacity: 1;
|
||||
max-height: 1000px;
|
||||
} */
|
||||
|
||||
93
public/styles/theme/chirac-theme.css
Normal file
@@ -0,0 +1,93 @@
|
||||
body.chirac {
|
||||
|
||||
background-image: url('../../assets/images/Chirac.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
|
||||
|
||||
--background-color: rgba(255, 255, 255, 0.85);
|
||||
--default-text-color: #002395;
|
||||
--subtitle-color: #EF4135;
|
||||
--mp3-text-color: #002395;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: rgba(255, 255, 255, 0.9);
|
||||
--form-input-bg-color: #f5f5f5;
|
||||
--form-input-border-color: #002395;
|
||||
--form-input-border-focus-color: #EF4135;
|
||||
--form-input-text-color: #002395;
|
||||
--form-input-placeholder-color: #999999;
|
||||
--form-button-bg-color: #002395;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #EF4135;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
--checkbox-bg-checked: #002395;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(0, 35, 149, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #002395;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: rgba(255, 255, 255, 0.9);
|
||||
--video-info-text-color: #000000;
|
||||
--video-info-heading-color: #EF4135;
|
||||
--video-info-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #002395;
|
||||
--video-info-link-hover-color: #EF4135;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #d1d1d1ca;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
Je serai le président de tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Mes chers compatriotes
|
||||
Je serai le président de tous les Français, de tous les Français
|
||||
Je mesure la difficulté de la tâche qui nous attend
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Je veux un État, je veux les Français
|
||||
Je veux un État, je veux
|
||||
Je veux un État-tat-tat-tat vigoureux
|
||||
Un État-tat-tat-tat impartial
|
||||
Un État-tat-tat-tat vigoureux
|
||||
Un État-tat-tat-tat exigeant
|
||||
Un État-tat-tat-tat
|
||||
Je serai le président de tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Un État qui n'isole pas, tous, pas, pas
|
||||
Tous, pas, tous, pas, pas
|
||||
Tous, pas, tous, pas, pas (les Français)
|
||||
Tous, tous, pas, tous, tous, pas, pas
|
||||
Tous, tous, pas, tous, tous, pas, pas
|
||||
Tous, tous, tous, pas, tous, pas, pas (les Français)
|
||||
Tous, pas, tous, tous, pas, pas
|
||||
Tous, tous, tous, pas, tous, tous, pas, pas
|
||||
Tous, tous, pas, tous, tous, pas, pas
|
||||
Tous, tous, tous-tous, tous-tous, pas, pas
|
||||
Tous, tous-tous, tous-tous, pas, pas
|
||||
Un État-tat-tat-tat vigoureux
|
||||
Un État-tat-tat-tat impartial
|
||||
Un État-tat-tat-tat exigeant
|
||||
Un État-tat-tat-tat
|
||||
Je serai le président de tous les Français
|
||||
|
||||
*/
|
||||
39
public/styles/theme/dark-theme.css
Normal file
@@ -0,0 +1,39 @@
|
||||
body.dark {
|
||||
/* Couleurs générales */
|
||||
--background-color: #121212; /* noir/gris très foncé */
|
||||
--default-text-color: #e0e0e0; /* texte clair */
|
||||
--subtitle-color: #2196f3; /* bleu accent */
|
||||
--mp3-text-color: #f5f5f5; /* texte checkbox */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #1e1e1e; /* fond formulaire */
|
||||
--form-input-bg-color: #2a2a2a; /* fond inputs */
|
||||
--form-input-border-color: #444444; /* bordure */
|
||||
--form-input-border-focus-color: #2196f3; /* focus bleu */
|
||||
--form-input-text-color: #f5f5f5; /* texte clair */
|
||||
--form-input-placeholder-color: #888888; /* placeholder gris */
|
||||
--form-button-bg-color: #2196f3; /* bouton bleu */
|
||||
--form-button-text-color: #ffffff; /* texte bouton */
|
||||
--form-button-bg-hover-color: #1976d2; /* hover bleu plus foncé */
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #555555; /* gris moyen */
|
||||
--checkbox-bg-checked: #2196f3; /* bleu accent */
|
||||
--checkbox-checkmark-border-color: #ffffff; /* blanc */
|
||||
--checkbox-pulse-color: rgba(33, 150, 243, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #2196f3;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #1a1a1a;
|
||||
--video-info-text-color: #e0e0e0;
|
||||
--video-info-heading-color: #2196f3;
|
||||
--video-info-list-color: #cccccc;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #2196f3;
|
||||
--video-info-link-hover-color: #1976d2;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #303030;
|
||||
}
|
||||
43
public/styles/theme/default-theme.css
Normal file
@@ -0,0 +1,43 @@
|
||||
:root {
|
||||
/* Couleurs générales */
|
||||
--background-color: #001224; /* body background bleu nuit */
|
||||
--default-text-color: #eee; /* texte clair principal */
|
||||
--subtitle-color: #007bff; /* sous-titres, header p */
|
||||
--mp3-text-color: #000000; /* couleur texte checkbox inline */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #ffffff; /* fond du formulaire */
|
||||
--form-input-bg-color: #f9f9f9; /* fond inputs */
|
||||
--form-input-border-color: #cccccc; /* bordure inputs */
|
||||
--form-input-border-focus-color: #0056b3; /* bordure focus */
|
||||
--form-input-text-color: #333333; /* texte inputs */
|
||||
--form-input-placeholder-color: #aaaaaa; /* placeholder */
|
||||
--form-button-bg-color: #007bff; /* bouton bg */
|
||||
--form-button-text-color: #ffffff; /* texte bouton */
|
||||
--form-button-bg-hover-color: #0056b3; /* bouton hover */
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
--checkbox-bg-checked: #3B82F6;
|
||||
--checkbox-checkmark-border-color: #E0E0E2;
|
||||
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #007bff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #0c0140;
|
||||
--video-info-text-color: #ffffff;
|
||||
--video-info-heading-color: #007bff;
|
||||
--video-info-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #070823a3;
|
||||
}
|
||||
|
||||
body.default .download-path #savePath {
|
||||
color: #ffffff;
|
||||
}
|
||||
51
public/styles/theme/drift-theme.css
Normal file
@@ -0,0 +1,51 @@
|
||||
body.drift {
|
||||
|
||||
background-image: url('../../assets/images/Drift.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Palette principale */
|
||||
--background-color: #0f1218;
|
||||
--default-text-color: #3d4b61;
|
||||
--subtitle-color: #1a1a1a;
|
||||
--mp3-text-color: #c0d4ff;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #1e222a;
|
||||
--form-input-bg-color: #13171d;
|
||||
--form-input-border-color: #3b4b6d;
|
||||
--form-input-border-focus-color: #6ea8ff;
|
||||
--form-input-text-color: #f0f4ff;
|
||||
--form-input-placeholder-color: #7a8fae;
|
||||
--form-button-bg-color: #2a2979;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #353393;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #2a2e38;
|
||||
--checkbox-bg-checked: #2a2979;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(76,108,255,0.3);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #ff8e8e;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #1e222ad0;
|
||||
--video-info-text-color: #e8e8f0;
|
||||
--video-info-heading-color: #9ab6ff;
|
||||
--video-info-list-color: #c0d4ff;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #4c6cff;
|
||||
--video-info-link-hover-color: #6c8dff;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #424b5cc2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
body.drift .download-path{
|
||||
color: #ededed;
|
||||
}
|
||||
39
public/styles/theme/fanatic-theme.css
Normal file
@@ -0,0 +1,39 @@
|
||||
body.fanatic{
|
||||
/* Couleurs générales */
|
||||
--background-color: #121212;
|
||||
--default-text-color: #eee;
|
||||
--subtitle-color: #FF5900;
|
||||
--mp3-text-color: #ffffff;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #1e1e1e;
|
||||
--form-input-bg-color: #2b2b2b;
|
||||
--form-input-border-color: #333333;
|
||||
--form-input-border-focus-color: #FF5900;
|
||||
--form-input-text-color: #eeeeee;
|
||||
--form-input-placeholder-color: #666666;
|
||||
--form-button-bg-color: #FF5900;
|
||||
--form-button-text-color: #121212;
|
||||
--form-button-bg-hover-color: #e65500;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #555555;
|
||||
--checkbox-bg-checked: #ff6600;
|
||||
--checkbox-checkmark-border-color: #eee;
|
||||
--checkbox-pulse-color: rgba(255, 102, 0, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #FF5900;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #222222;
|
||||
--video-info-text-color: #eee;
|
||||
--video-info-heading-color: #FF5900;
|
||||
--video-info-list-color: #ccc;
|
||||
--video-info-list-strong-color: #fff;
|
||||
--video-info-link-color: #FF5900;
|
||||
--video-info-link-hover-color: #e65500;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #2b2b2b;
|
||||
}
|
||||
39
public/styles/theme/light-theme.css
Normal file
@@ -0,0 +1,39 @@
|
||||
body.light {
|
||||
/* Couleurs générales */
|
||||
--background-color: #dadada;
|
||||
--default-text-color: #222222;
|
||||
--subtitle-color: #007bff;
|
||||
--mp3-text-color: #000000;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #ffffff;
|
||||
--form-input-bg-color: #fafafa;
|
||||
--form-input-border-color: #cccccc;
|
||||
--form-input-border-focus-color: #007bff;
|
||||
--form-input-text-color: #333333;
|
||||
--form-input-placeholder-color: #888888;
|
||||
--form-button-bg-color: #007bff;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #0056b3;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
--checkbox-bg-checked: #007bff;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(0, 123, 255, 0.4);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #007bff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #ffffff;
|
||||
--video-info-text-color: #222222;
|
||||
--video-info-heading-color: #007bff;
|
||||
--video-info-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #c5c5c5b2;
|
||||
}
|
||||
50
public/styles/theme/neon-theme.css
Normal file
@@ -0,0 +1,50 @@
|
||||
body.neon {
|
||||
|
||||
background: (url("../../assets/images/drift.jpg"));
|
||||
background-image: linear-gradient(135deg, #0f0c29, #302b63, #24243e); /* dégradé sombre violet/bleu */
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Couleurs générales */
|
||||
--background-color: #1b1b2f;
|
||||
--default-text-color: #e0e0ff;
|
||||
--subtitle-color: #a3a3ff;
|
||||
--mp3-text-color: #c0c0ff;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #2c2c4a;
|
||||
--form-input-bg-color: #1f1f3a;
|
||||
--form-input-border-color: #5858f0;
|
||||
--form-input-border-focus-color: #a3a3ff;
|
||||
--form-input-text-color: #ffffff;
|
||||
--form-input-placeholder-color: #8888ff;
|
||||
--form-button-bg-color: #5858f0;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #7a7aff;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #302b63;
|
||||
--checkbox-bg-checked: #5858f0;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(88,88,240,0.3);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #a3a3ff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #2c2c4a;
|
||||
--video-info-text-color: #e0e0ff;
|
||||
--video-info-heading-color: #a3a3ff;
|
||||
--video-info-list-color: #c0c0ff;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #5858f0;
|
||||
--video-info-link-hover-color: #7a7aff;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #3e3e64;
|
||||
}
|
||||
|
||||
body.neon .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
49
public/styles/theme/nf-theme.css
Normal file
@@ -0,0 +1,49 @@
|
||||
body.nf {
|
||||
|
||||
background-image: url('../../assets/images/NF.jpg');
|
||||
background-size: cover;
|
||||
background-position: 70% center;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Couleurs générales */
|
||||
--background-color: #1a1a1a;
|
||||
--default-text-color: #e6dfd5;
|
||||
--subtitle-color: #f5f0e6;
|
||||
--mp3-text-color: #d8cfc2;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #2a2a2a;
|
||||
--form-input-bg-color: #222222;
|
||||
--form-input-border-color: #4a4036;
|
||||
--form-input-border-focus-color: #cbb89d;
|
||||
--form-input-text-color: #f0e8dd;
|
||||
--form-input-placeholder-color: #9a8f80;
|
||||
--form-button-bg-color: #cbb89d;
|
||||
--form-button-text-color: #1a1a1a;
|
||||
--form-button-bg-hover-color: #b7a588;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #4a4036;
|
||||
--checkbox-bg-checked: #cbb89d;
|
||||
--checkbox-checkmark-border-color: #1a1a1a;
|
||||
--checkbox-pulse-color: rgba(203,184,157,0.3);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #cbb89d;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #2a2a2a;
|
||||
--video-info-text-color: #e6dfd5;
|
||||
--video-info-heading-color: #f5f0e6;
|
||||
--video-info-list-color: #d8cfc2;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #cbb89d;
|
||||
--video-info-link-hover-color: #b7a588;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #4f4f4f;
|
||||
}
|
||||
|
||||
body.nf .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
49
public/styles/theme/songbird-theme.css
Normal file
@@ -0,0 +1,49 @@
|
||||
body.songbird {
|
||||
|
||||
background-image: url('../../assets/images/Songbird.webp');
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Couleurs générales */
|
||||
--background-color: #0a0a0a; /* noir profond */
|
||||
--default-text-color: #ff2a2a; /* rouge néon */
|
||||
--subtitle-color: #f2f2f2; /* rouge sombre */
|
||||
--mp3-text-color: #ff4d4d; /* rouge un peu plus clair */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: rgba(20,0,0,0.85); /* noir semi-transparent avec rouge */
|
||||
--form-input-bg-color: #1a0a0a;
|
||||
--form-input-border-color: #ff2a2a;
|
||||
--form-input-border-focus-color: #b30000;
|
||||
--form-input-text-color: #ffffff;
|
||||
--form-input-placeholder-color: #950404;
|
||||
--form-button-bg-color: #b30000;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #ff2a2a;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #630000;
|
||||
--checkbox-bg-checked: #ff2a2a;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(255,42,42,0.3);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #ff2a2a;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: rgba(30,0,0,0.9);
|
||||
--video-info-text-color: #ff4d4d;
|
||||
--video-info-heading-color: #ff2a2a;
|
||||
--video-info-list-color: #b30000;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #ff2a2a;
|
||||
--video-info-link-hover-color: #b30000;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #3f0000;
|
||||
}
|
||||
|
||||
body.cyberpunk .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
48
public/styles/theme/spicy-theme.css
Normal file
@@ -0,0 +1,48 @@
|
||||
body.spicy {
|
||||
/* Couleurs générales */
|
||||
--background-color: #121212; /* fond noir profond */
|
||||
--default-text-color: #ffffff; /* texte clair */
|
||||
--subtitle-color: #f87918; /* accent rouge piment */
|
||||
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #1a1a1a; /* fond formulaire gris très foncé */
|
||||
--form-input-bg-color: #2b2b2b; /* fond inputs gris foncé */
|
||||
--form-input-border-color: #444444; /* bordure gris */
|
||||
--form-input-border-focus-color: #ff1a1a; /* focus rouge piment */
|
||||
--form-input-text-color: #eeeeee; /* texte inputs */
|
||||
--form-input-placeholder-color: #888888; /* placeholder gris moyen */
|
||||
--form-button-bg-color: #ff1a1a; /* bouton rouge vif */
|
||||
--form-button-text-color: #ffffff; /* texte bouton blanc */
|
||||
--form-button-bg-hover-color: #cc0000; /* hover rouge foncé */
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #555555; /* gris moyen */
|
||||
--checkbox-bg-checked: #ff1a1a; /* rouge piment */
|
||||
--checkbox-checkmark-border-color: #f5f5f5; /* blanc cassé */
|
||||
--checkbox-pulse-color: rgba(255, 26, 26, 0.5); /* pulse rouge translucide */
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #ffffff; /* rouge vif */
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #1a1a1a; /* fond infos gris foncé */
|
||||
--video-info-text-color: #f5f5f5; /* texte clair */
|
||||
--video-info-heading-color: #ff1a1a; /* titres rouges */
|
||||
--video-info-list-color: #cccccc; /* texte liste gris clair */
|
||||
--video-info-list-strong-color: #ffffff; /* texte important blanc */
|
||||
--video-info-link-color: #ff1a1a; /* liens rouges */
|
||||
--video-info-link-hover-color: #cc0000; /* liens hover rouge foncé */
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #353232;
|
||||
}
|
||||
|
||||
/* Optionnel: background piments */
|
||||
body.spicy {
|
||||
background-image: url('../../assets/images/Spicy.png');
|
||||
background-size:cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
10
public/styles/theme/themeimport.css
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
@import url("chirac-theme.css");
|
||||
@import url("dark-theme.css");
|
||||
@import url("default-theme.css");
|
||||
@import url("fanatic-theme.css");
|
||||
@import url("light-theme.css");
|
||||
@import url("nf-theme.css");
|
||||
@import url("drift-theme.css");
|
||||
@import url("neon-theme.css");
|
||||
@import url("songbird-theme.css");
|
||||
@import url("spicy-theme.css")
|
||||
@@ -1,11 +1,36 @@
|
||||
:root{
|
||||
--background-color : #0F172A;
|
||||
--infos-box-color: #f1f1f1 ;
|
||||
--title-color: ;
|
||||
--subtitle-color : #a3aab4;
|
||||
--mp3-text-color: #0F172A;
|
||||
--default-text-color: #F8FAFC;
|
||||
:root {
|
||||
/* Couleurs générales */
|
||||
--background-color: #001224;
|
||||
--default-text-color: #eee;
|
||||
--subtitle-color: #007bff;
|
||||
--mp3-text-color: #000000;
|
||||
|
||||
--checkbox-pulse-color:#9bbcf1;
|
||||
/* Formulaire */
|
||||
--form-bg-color: #ffffff;
|
||||
--form-input-bg-color: #f9f9f9;
|
||||
--form-input-border-color: #cccccc;
|
||||
--form-input-border-focus-color: #0056b3;
|
||||
--form-input-text-color: #333333;
|
||||
--form-input-placeholder-color: #aaaaaa;
|
||||
--form-button-bg-color: #007bff;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #0056b3;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
--checkbox-bg-checked: #3B82F6;
|
||||
--checkbox-checkmark-border-color: #E0E0E2;
|
||||
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #007bff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #f0f0f0;
|
||||
--video-info-text-color: #222222;
|
||||
--video-info-heading-color: #007bff;
|
||||
--video-info-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
}
|
||||
BIN
ressources/yt-dlp.exe
Normal file
65
server/controller/download.controller.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const { fetchDownload } = require("../services/download.services");
|
||||
const { logger } = require("../logger");
|
||||
const { isValidUrl, isSafePath } = require("../helpers/validation");
|
||||
const { buildYtDlpArgs } = require("../helpers/buildArgs");
|
||||
const { notifyDownloadFinished } = require("../helpers/notify");
|
||||
|
||||
const listeners = [];
|
||||
const speedListeners = [];
|
||||
|
||||
async function downloadController(req, res) {
|
||||
try {
|
||||
const options = {
|
||||
url: req.body.url,
|
||||
audioOnly: req.body.audioOnly === "1",
|
||||
quality: req.body.quality || "best",
|
||||
outputFolder: req.body.savePath || undefined,
|
||||
};
|
||||
|
||||
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("❌ Invalid URL !");
|
||||
if (options.outputFolder && !isSafePath(options.outputFolder)) return res.status(400).send("❌ Save Path Not Allowed.");
|
||||
|
||||
const filePath = await fetchDownload(options, listeners, speedListeners);
|
||||
notifyDownloadFinished(filePath);
|
||||
res.send("✅ Download Done !");
|
||||
|
||||
} catch (err) {
|
||||
logger.error(`Server Error in /download : ${err.message}`);
|
||||
res.status(500).send(`❌ ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function progressController(req, res) {
|
||||
res.set({
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
});
|
||||
|
||||
const sendProgress = percent => res.write(`data: ${percent}\n\n`);
|
||||
listeners.push(sendProgress);
|
||||
|
||||
req.on('close', () => {
|
||||
listeners.splice(listeners.indexOf(sendProgress), 1);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
function speedController(req, res) {
|
||||
res.set({
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
});
|
||||
|
||||
const sendSpeed = speed => res.write(`data: ${speed}\n\n`);
|
||||
speedListeners.push(sendSpeed);
|
||||
|
||||
req.on('close', () => {
|
||||
speedListeners.splice(speedListeners.indexOf(sendSpeed), 1);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = { downloadController, progressController, speedController};
|
||||
48
server/controller/info.controller.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { fetchInfo } = require("../services/info.services");
|
||||
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo");
|
||||
const { logger } = require("../logger");
|
||||
const { isValidUrl } = require("../helpers/validation");
|
||||
|
||||
async function infoController(req, res){
|
||||
|
||||
const url = req.body.url || req.query.url; // Gérer POST et GET
|
||||
|
||||
|
||||
/* Si pas d'url ou url invalide*/
|
||||
if (!url || !isValidUrl(url)) return res.status(400).send("❌ Invalid URL Or Missing");
|
||||
|
||||
logger.info(`/Info Request receive by the Info Controller. URL: ${url}`);
|
||||
|
||||
if (url.includes("?list")) {
|
||||
logger.info("Estimated Data Type: Playlist")
|
||||
|
||||
} else{
|
||||
logger.info("Estimated Data Type: Video")
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fetchInfo(url);
|
||||
|
||||
if (data._type === "playlist") {
|
||||
|
||||
const playlist = parsePlaylist(data);
|
||||
logger.info(`Playlist : ${playlist.title} (${playlist.count} vidéos)`);
|
||||
return res.json(playlist);
|
||||
|
||||
} else{
|
||||
const video = parseVideo(data);
|
||||
logger.info(`Unique Video: ${video.title}`);
|
||||
return res.json(video);
|
||||
}
|
||||
|
||||
|
||||
} catch (err) {
|
||||
return res.status(500).send(`❌ ${err.message}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {infoController};
|
||||
52
server/discordRPC.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const config = require('../config');
|
||||
const RPC = require("discord-rpc");
|
||||
const { logger } = require("./logger");
|
||||
|
||||
const clientId = `${config.DiscordRPCID}`;
|
||||
const rpc = new RPC.Client({ transport: "ipc" });
|
||||
|
||||
let intervalId;
|
||||
|
||||
function startRPC() {
|
||||
rpc.on("ready", () => {
|
||||
const presence = {
|
||||
largeImageKey: "icon",
|
||||
smallImageKey: "acnolo_pfp",
|
||||
smallImageText: "By MasterAcnolo",
|
||||
startTimestamp: new Date(),
|
||||
details: `Open Source Download Tools - ${config.version}`,
|
||||
state: "masteracnolo.github.io/No-Sense/FreedomLoader",
|
||||
};
|
||||
|
||||
rpc.setActivity(presence);
|
||||
|
||||
// Met à jour la présence toutes les 15s
|
||||
intervalId = setInterval(() => {
|
||||
rpc.setActivity(presence);
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
async function cleanExit() {
|
||||
try {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
|
||||
if (rpc && rpc.transport) {
|
||||
await rpc.destroy();
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error while closing the RPC:", err);
|
||||
} finally {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
process.on("exit", cleanExit);
|
||||
process.on("SIGINT", cleanExit);
|
||||
process.on("SIGTERM", cleanExit);
|
||||
|
||||
rpc.login({ clientId }).catch(err => {
|
||||
logger.error("Unable to connect to the RPC:", err);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { startRPC };
|
||||
43
server/helpers/buildArgs.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const path = require("path");
|
||||
|
||||
const getUserBrowser = require("./getBrowser");
|
||||
const { ffmpegPath, denoPath} = require("./path");
|
||||
|
||||
|
||||
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||
|
||||
const args = [
|
||||
"--cookies-from-browser", `${getUserBrowser()}`,
|
||||
"--no-continue",
|
||||
"--no-overwrites",
|
||||
"--embed-thumbnail",
|
||||
"--add-metadata",
|
||||
"--concurrent-fragments", "8",
|
||||
"--retries", "10",
|
||||
"--fragment-retries", "10",
|
||||
"--ffmpeg-location", ffmpegPath,
|
||||
"--extractor-args","youtube:player_client=default",
|
||||
"--js-runtimes", `deno:${denoPath}`
|
||||
];
|
||||
|
||||
if (audioOnly) args.push("-f", "bestaudio", "--extract-audio", "--audio-format", "mp3");
|
||||
else args.push("--merge-output", "mp4");
|
||||
|
||||
const qualityMap = {
|
||||
best: "bestvideo+bestaudio/best/mp4",
|
||||
medium: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
|
||||
worst: "worstvideo+worstaudio/worst/mp4",
|
||||
1080: "bestvideo[height<=1080]+bestaudio/best[height<=1080]/mp4",
|
||||
720: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
|
||||
480: "bestvideo[height<=480]+bestaudio/best[height<=480]/mp4",
|
||||
};
|
||||
|
||||
args.push("-f", qualityMap[quality] || "best");
|
||||
args.push("-o", path.join(outputFolder, "%(title)s.%(ext)s"));
|
||||
args.push(url);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
module.exports = { buildYtDlpArgs };
|
||||
|
||||
32
server/helpers/getBrowser.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const notify = require("./notify")
|
||||
|
||||
function getUserBrowser() {
|
||||
const userProfile = os.homedir();
|
||||
|
||||
// Liste des navigateurs supportés par yt-dlp (Actuellement je peux que garantir Firefox, Désolé)
|
||||
const browsers = [
|
||||
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
|
||||
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
|
||||
// { name: "brave", path: path.join(userProfile, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default") },
|
||||
// { name: "edge", path: path.join(userProfile, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default") },
|
||||
// { name: "opera", path: path.join(userProfile, "AppData", "Roaming", "Opera Software", "Opera Stable") },
|
||||
// { name: "vivaldi", path: path.join(userProfile, "AppData", "Local", "Vivaldi", "User Data", "Default") },
|
||||
// { name: "safari", path: path.join(userProfile, "AppData", "Local", "Apple Computer", "Safari") },
|
||||
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
|
||||
];
|
||||
|
||||
for (const browser of browsers) {
|
||||
if (fs.existsSync(browser.path)) {
|
||||
return browser.name;
|
||||
} else{
|
||||
notify.notifyFirefoxBrowserMissing();
|
||||
}
|
||||
}
|
||||
notify.notifyCookiesBrowserError()
|
||||
return ;
|
||||
}
|
||||
|
||||
module.exports = getUserBrowser
|
||||
44
server/helpers/notify.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const { Notification, shell } = require("electron");
|
||||
const path = require("path");
|
||||
|
||||
function notifyDownloadFinished(folder) {
|
||||
const iconPath = path.join(process.resourcesPath, "confirm-icon.png");
|
||||
const notif = new Notification({
|
||||
title: "Freedom Loader",
|
||||
body: "Your download is complete, click here to open it.",
|
||||
icon: iconPath,
|
||||
});
|
||||
|
||||
notif.on("click", () => shell.openPath(folder));
|
||||
notif.show();
|
||||
}
|
||||
|
||||
function notifyCookiesBrowserError(){
|
||||
const iconPath = path.join(process.resourcesPath, "error.png");
|
||||
const notif = new Notification({
|
||||
title: "Cookies Error",
|
||||
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.",
|
||||
icon: iconPath,
|
||||
});
|
||||
|
||||
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"));
|
||||
notif.show();
|
||||
}
|
||||
|
||||
function notifyFirefoxBrowserMissing(){
|
||||
const iconPath = path.join(process.ressourcesPath, "error.png");
|
||||
const notif = new Notification({
|
||||
title: "Firefox Missing",
|
||||
body: "Firefox was not found on your system. Click here to follow the installation guide",
|
||||
icon: iconPath,
|
||||
});
|
||||
|
||||
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"))
|
||||
notif.show();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
notifyDownloadFinished,
|
||||
notifyCookiesBrowserError,
|
||||
notifyFirefoxBrowserMissing
|
||||
};
|
||||
33
server/helpers/parseInfo.js
Normal file
@@ -0,0 +1,33 @@
|
||||
function parseVideo(data) {
|
||||
|
||||
return {
|
||||
type: "video",
|
||||
// id: data.id,
|
||||
// title: data.title,
|
||||
// url: data.webpage_url,
|
||||
// duration: data.duration,
|
||||
// thumbnail: data.thumbnail,
|
||||
// uploader: data.uploader
|
||||
...data
|
||||
};
|
||||
}
|
||||
|
||||
function parsePlaylist(data) {
|
||||
|
||||
return {
|
||||
type: "playlist",
|
||||
title: data.title || data.id,
|
||||
channel: data.uploader,
|
||||
count: data.entries.length,
|
||||
videos: (data.entries || []).map(v => ({
|
||||
id: v.id,
|
||||
title: v.title,
|
||||
url: v.webpage_url,
|
||||
duration: v.duration,
|
||||
thumbnail: v.thumbnail,
|
||||
uploader: v.uploader
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { parseVideo, parsePlaylist };
|
||||
26
server/helpers/path.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { app } = require("electron");
|
||||
const config = require("../../config");
|
||||
|
||||
let userYtDlp;
|
||||
let ffmpegPath;
|
||||
let denoPath;
|
||||
|
||||
const sourceYtDlp = path.join(process.resourcesPath, "yt-dlp.exe");
|
||||
|
||||
if (config.localMode) {
|
||||
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe");
|
||||
ffmpegPath = path.join(__dirname, "../../ressources"); // <- contient ffmpeg.exe et ffprobe.exe
|
||||
denoPath = path.join(__dirname, "../../ressources/deno.exe");
|
||||
} else {
|
||||
userYtDlp = path.join(app.getPath("userData"), "yt-dlp.exe");
|
||||
ffmpegPath = path.join(process.resourcesPath, "ffmpeg.exe");
|
||||
denoPath = path.join(process.resourcesPath, "deno.exe");
|
||||
|
||||
if (!fs.existsSync(userYtDlp)) {
|
||||
fs.copyFileSync(sourceYtDlp, userYtDlp);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { userYtDlp, ffmpegPath, denoPath };
|
||||
21
server/helpers/validation.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const path = require("path");
|
||||
|
||||
function isValidUrl(url) {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function isSafePath(folder) {
|
||||
if (!folder || folder.length < 3) return false;
|
||||
const unsafe = ["System32", "/etc", "\\Windows"];
|
||||
const resolved = path.resolve(folder);
|
||||
return !unsafe.some(u => resolved.includes(u));
|
||||
}
|
||||
|
||||
module.exports = { isValidUrl, isSafePath };
|
||||
102
server/logger.js
@@ -1,93 +1,61 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
|
||||
Freedom Loader is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
|
||||
Freedom Loader is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const { createLogger, format, transports } = require("winston");
|
||||
const DailyRotateFile = require("winston-daily-rotate-file");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const config = require("../config")
|
||||
|
||||
const fs = require("fs"); // pour vérifier/créer le dossier de logs
|
||||
const path = require("path"); // pour gérer proprement les chemins
|
||||
// Dossier de logs Windows
|
||||
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
|
||||
|
||||
// on définit le dossier de logs
|
||||
const logDir = path.join(__dirname, "../logs");
|
||||
// on le crée si inexistant
|
||||
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir);
|
||||
// Création du dossier "logs" si nécessaire
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
|
||||
// fonction pour générer une ligne de début de session
|
||||
function getSessionStartLine() {
|
||||
const now = new Date().toISOString();
|
||||
return `--- Démarrage de la session : ${now} ---`;
|
||||
}
|
||||
const logFormat = format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
||||
);
|
||||
|
||||
// fonction pour générer une ligne de fin de session
|
||||
function getSessionEndLine() {
|
||||
const now = new Date().toISOString();
|
||||
return `--- Fin de la session : ${now} ---`;
|
||||
}
|
||||
|
||||
// configuration principale de Winston
|
||||
// Configuration du logger
|
||||
const logger = createLogger({
|
||||
level: "info", // niveau de log minimum (info, warn, error, etc.)
|
||||
format: format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), // timestamp lisible
|
||||
format.printf(({ timestamp, level, message }) =>
|
||||
`${timestamp} | ${level.toUpperCase()} | ${message}` // format de ligne de log
|
||||
)
|
||||
),
|
||||
level: "info",
|
||||
format: logFormat,
|
||||
transports: [
|
||||
new DailyRotateFile({
|
||||
dirname: logDir, // dossier de logs
|
||||
filename: "LOGS-%DATE%.log", // nom de fichier journalier
|
||||
datePattern: "YYYY-MM-DD", // pattern de rotation
|
||||
zippedArchive: false, // true = compression des archives
|
||||
maxFiles: "14d", // garde 14 jours d’historique
|
||||
format: format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) =>
|
||||
`${timestamp} | ${level.toUpperCase()} | ${message}`
|
||||
)
|
||||
),
|
||||
options: { flags: "a" }, // mode append
|
||||
dirname: logDir,
|
||||
filename: "LOGS-%DATE%.log",
|
||||
datePattern: "YYYY-MM-DD",
|
||||
zippedArchive: false,
|
||||
maxFiles: "7d",
|
||||
format: logFormat,
|
||||
options: { flags: "a" },
|
||||
}),
|
||||
new transports.Console({
|
||||
format: format.combine(
|
||||
format.colorize(),
|
||||
format.timestamp({ format: "HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) =>
|
||||
`${timestamp} | ${level} | ${message}`
|
||||
)
|
||||
),
|
||||
format: logFormat,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// fonction d'utilitaire pour marquer le début de la session
|
||||
function logSessionStart() {
|
||||
logger.info(getSessionStartLine());
|
||||
function getSessionStartLine() {
|
||||
return `--- Starting session: ${new Date().toISOString()} ---`;
|
||||
}
|
||||
|
||||
function getSessionEndLine() {
|
||||
return `--- Ending session: ${new Date().toISOString()} ---`;
|
||||
}
|
||||
|
||||
function logSessionStart() {
|
||||
logger.info(getSessionStartLine());
|
||||
logger.info(`Application Version: ${config.version}`)
|
||||
}
|
||||
|
||||
// pour la fin de la session
|
||||
function logSessionEnd() {
|
||||
logger.info(getSessionEndLine());
|
||||
}
|
||||
|
||||
// exporte le logger et les helpers pour les autres modules
|
||||
module.exports = {
|
||||
logger,
|
||||
logSessionStart,
|
||||
logSessionEnd,
|
||||
logDir,
|
||||
};
|
||||
@@ -1,165 +0,0 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
|
||||
Freedom Loader is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
|
||||
Freedom Loader is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const express = require("express"); // importe le framework Express
|
||||
const router = express.Router(); // crée un routeur Express pour modulariser les routes
|
||||
const { execFile } = require("child_process"); // permet d’exécuter un binaire externe sans passer par un shell
|
||||
const path = require("path"); // module Node.js pour gérer les chemins de fichiers
|
||||
const fs = require("fs"); // module Node.js pour manipuler le système de fichiers
|
||||
|
||||
const logger = require("../logger").logger; // récupère l'instance de logger Winston personnalisée
|
||||
|
||||
// chemin vers l’exécutable yt-dlp packagé avec l’appli
|
||||
const ytDlpPath = path.join(__dirname, '../../yt-dlp.exe');
|
||||
|
||||
// route POST sur la racine (ex: POST /download)
|
||||
router.post("/", (req, res) => {
|
||||
try {
|
||||
// récupère les options envoyées depuis le frontend (via formulaire ou requête AJAX)
|
||||
const options = {
|
||||
url: req.body.url, // URL à télécharger
|
||||
audioOnly: req.body.audioOnly === "1", // conversion audio uniquement (mp3)
|
||||
quality: req.body.quality || "best", // qualité vidéo à télécharger
|
||||
subtitles: req.body.subs === "1", // récupérer les sous-titres anglais
|
||||
};
|
||||
|
||||
if (!options.url) {
|
||||
// si l’URL est absente, on log et on renvoie 400
|
||||
logger.warn("Requête POST /download sans URL");
|
||||
return res.status(400).send("❌ URL manquante !");
|
||||
}
|
||||
|
||||
// on récupère le dossier de sortie depuis app.locals (défini ailleurs dans le serveur)
|
||||
const outputFolder = req.app.locals.outputFolder;
|
||||
|
||||
// s'il n'existe pas, on le crée automatiquement
|
||||
if (!fs.existsSync(outputFolder)) {
|
||||
fs.mkdirSync(outputFolder, { recursive: true });
|
||||
logger.info("Dossier Freedom Loader Output créé à la volée dans Téléchargements.");
|
||||
}
|
||||
|
||||
// template du nom de fichier de sortie (ex: MonTitre.mp4 ou MonTitre.mp3)
|
||||
const outputTemplate = path.join(outputFolder, "%(title)s.%(ext)s");
|
||||
|
||||
// tableau d'arguments pour yt-dlp
|
||||
const args = [];
|
||||
|
||||
if (options.audioOnly) {
|
||||
// si audioOnly, on force extraction audio en mp3
|
||||
args.push("--extract-audio", "--audio-format", "mp3");
|
||||
}
|
||||
|
||||
if (options.subtitles) {
|
||||
// si l’utilisateur veut des sous-titres
|
||||
args.push("--write-subs", "--sub-lang", "en");
|
||||
}
|
||||
|
||||
// // qualité souhaitée
|
||||
// args.push("-f", options.quality);
|
||||
|
||||
// let format;
|
||||
// if (options.quality === "best") {
|
||||
// format = "bestvideo+bestaudio/best";
|
||||
// } else if (options.quality === "worst") {
|
||||
// format = "worstvideo+worstaudio/worst";
|
||||
// } else {
|
||||
// format = "best"; // fallback
|
||||
// }
|
||||
// args.push("-f", format);
|
||||
|
||||
args.push("--no-continue");
|
||||
|
||||
let format;
|
||||
|
||||
switch (options.quality) {
|
||||
case "best":
|
||||
format = "bestvideo+bestaudio/best";
|
||||
break;
|
||||
case "medium":
|
||||
// qualité moyenne, ex: 720p vidéo + meilleur audio
|
||||
format = "bestvideo[height<=720]+bestaudio/best[height<=720]";
|
||||
break;
|
||||
case "worst":
|
||||
format = "worstvideo+worstaudio/worst";
|
||||
break;
|
||||
case "1080":
|
||||
format = "bestvideo[height<=1080]+bestaudio/best[height<=1080]";
|
||||
break;
|
||||
case "720":
|
||||
format = "bestvideo[height<=720]+bestaudio/best[height<=720]";
|
||||
break;
|
||||
case "480":
|
||||
format = "bestvideo[height<=480]+bestaudio/best[height<=480]";
|
||||
break;
|
||||
default:
|
||||
format = "best";
|
||||
}
|
||||
args.push("-f", format);
|
||||
|
||||
// modèle de sortie
|
||||
args.push("-o", outputTemplate);
|
||||
// enfin, l'URL à télécharger
|
||||
args.push(options.url);
|
||||
|
||||
// log de la commande complète pour debug
|
||||
logger.info(`Commande yt-dlp : ${ytDlpPath} ${args.join(" ")}`);
|
||||
|
||||
// exécution du binaire yt-dlp
|
||||
const child = execFile(ytDlpPath, args);
|
||||
|
||||
// gestion des logs stdout de yt-dlp
|
||||
child.stdout.on("data", (data) => {
|
||||
data.toString().split("\n").forEach(line => {
|
||||
if(line.trim()) logger.info(`[yt-dlp stdout] ${line.trim()}`);
|
||||
});
|
||||
});
|
||||
|
||||
// gestion des logs stderr de yt-dlp (les erreurs)
|
||||
child.stderr.on("data", (data) => {
|
||||
data.toString().split("\n").forEach(line => {
|
||||
if(line.trim()) logger.error(`[yt-dlp stderr] ${line.trim()}`);
|
||||
});
|
||||
});
|
||||
|
||||
// si une erreur système survient (par ex : yt-dlp.exe introuvable)
|
||||
child.on("error", (err) => {
|
||||
logger.error(`Erreur lancement yt-dlp : ${err.message}`);
|
||||
res.status(500).send(`❌ Erreur lors de l'exécution : ${err.message}`);
|
||||
});
|
||||
|
||||
// callback quand le processus se termine
|
||||
child.on("close", (code) => {
|
||||
logger.info(`yt-dlp terminé avec code de sortie : ${code}`);
|
||||
if(code === 0) {
|
||||
// succès
|
||||
res.send("✅ Téléchargement terminé !");
|
||||
} else {
|
||||
// yt-dlp a échoué
|
||||
res.status(500).send(`❌ yt-dlp a échoué avec le code : ${code}`);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
// en cas d'erreur JavaScript
|
||||
logger.error(`Erreur serveur dans /download : ${err.message}`);
|
||||
res.status(500).send(`Erreur serveur : ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// on exporte ce routeur pour qu'il soit utilisé par l'app principale
|
||||
module.exports = router;
|
||||
11
server/routes/download.route.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { downloadController, progressController, speedController } = require("../controller/download.controller");
|
||||
|
||||
router.post("/", downloadController);
|
||||
|
||||
// SSE pour la progression du téléchargement
|
||||
router.get("/progress", progressController);
|
||||
router.get("/speed", speedController);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
|
||||
Freedom Loader is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
|
||||
Freedom Loader is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const express = require("express"); // importe Express
|
||||
const router = express.Router(); // crée un routeur Express indépendant
|
||||
const { execFile } = require("child_process"); // permet d’exécuter un programme externe
|
||||
const path = require("path"); // module pour gérer les chemins de fichiers
|
||||
|
||||
// chemin vers le binaire yt-dlp embarqué
|
||||
const ytDlpPath = path.join(__dirname, "../../yt-dlp.exe");
|
||||
|
||||
// route POST sur la racine de ce routeur
|
||||
router.post("/", (req, res) => {
|
||||
const url = req.body.url; // récupère l'URL depuis la requête POST
|
||||
if (!url) return res.status(400).send("❌ URL manquante"); // validation simple
|
||||
|
||||
// exécute yt-dlp avec --dump-json pour récupérer les métadonnées de la vidéo
|
||||
execFile(ytDlpPath, ["--dump-json", url], (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
// gestion d’erreur : par exemple yt-dlp plante ou l’URL est invalide
|
||||
console.error("Erreur lors de l'exécution de yt-dlp :", error);
|
||||
console.error("stderr :", stderr);
|
||||
return res.status(500).send("❌ Impossible de récupérer les infos.");
|
||||
}
|
||||
|
||||
try {
|
||||
// on parse le JSON envoyé par yt-dlp
|
||||
const info = JSON.parse(stdout);
|
||||
// on renvoie l’objet JSON directement au frontend
|
||||
res.json(info);
|
||||
} catch (e) {
|
||||
// le JSON est mal formé
|
||||
console.error("Erreur lors du parsing JSON :", e);
|
||||
res.status(500).send("❌ JSON illisible.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// on exporte le routeur pour être utilisé dans app.js ou ailleurs
|
||||
module.exports = router;
|
||||
7
server/routes/info.route.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { infoController } = require("../controller/info.controller");
|
||||
|
||||
router.post("/", infoController);
|
||||
|
||||
module.exports = router;
|
||||
127
server/server.js
@@ -1,103 +1,70 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
|
||||
Freedom Loader is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
|
||||
Freedom Loader is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const express = require("express"); // framework web
|
||||
const { exec } = require("child_process"); // si besoin d’exécuter des commandes (pas utilisé ici directement)
|
||||
const fs = require("fs"); // accès au système de fichiers
|
||||
const path = require("path"); // gestion de chemins
|
||||
const debug = require("debug")("freedom-loader:server");
|
||||
// petit debugger coloré en console
|
||||
|
||||
const express = require("express");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { logger, logSessionStart, logSessionEnd } = require("./logger");
|
||||
// récupère notre logger Winston + helpers de session
|
||||
const config = require("../config");
|
||||
const { execFile } = require("child_process");
|
||||
const { userYtDlp } = require("./helpers/path");
|
||||
|
||||
const app = express(); // instancie l'application Express
|
||||
|
||||
// logSessionStart(); // désactivé, mais pourrait logguer le début de session
|
||||
const app = express();
|
||||
|
||||
// on définit le dossier de téléchargement dans le dossier Téléchargements de l’utilisateur
|
||||
const downloadsPath = path.join(process.env.USERPROFILE, "Downloads");
|
||||
const outputFolder = path.join(downloadsPath, "Freedom Loader Output");
|
||||
app.use(express.json());
|
||||
|
||||
// création du dossier s’il n’existe pas
|
||||
if (!fs.existsSync(outputFolder)) {
|
||||
try {
|
||||
fs.mkdirSync(outputFolder, { recursive: true });
|
||||
logger.info("Dossier Freedom Loader Output cree dans Telechargements.");
|
||||
} catch (err) {
|
||||
logger.error("Impossible de creer le dossier :", err);
|
||||
process.exit(1); // arrêt du programme en cas d’échec
|
||||
}
|
||||
} else {
|
||||
logger.info("Dossier Freedom Loader Output deja existant.");
|
||||
// Dossier de téléchargement
|
||||
const outputFolder = path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader");
|
||||
|
||||
// Création du dossier si nécessaire
|
||||
try {
|
||||
fs.mkdirSync(outputFolder, { recursive: true });
|
||||
logger.info(`Freedom Loader folder ready in ${outputFolder}`);
|
||||
} catch (err) {
|
||||
logger.error("Unable to create folder:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// on rend ce dossier dispo via app.locals
|
||||
app.locals.outputFolder = outputFolder;
|
||||
// Mise à jour yt-dlp au
|
||||
execFile(userYtDlp, ["-U"], (err, stdout, stderr) => {
|
||||
if (err) logger.warn("yt-dlp update error:", err);
|
||||
else logger.info(`Update yt-dlp : ${stdout}`);
|
||||
});
|
||||
|
||||
// configuration pour parser les requêtes POST en urlencoded
|
||||
// Middlewares
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.static(path.join(__dirname, "../public")));
|
||||
|
||||
// sert les fichiers statiques (frontend)
|
||||
const staticPath = path.join(__dirname, "../public");
|
||||
debug("Serveur statique sur", staticPath);
|
||||
app.use(express.static(staticPath));
|
||||
// Routes
|
||||
app.use("/download", require("./routes/download.route"));
|
||||
app.use("/info", require("./routes/info.route"));
|
||||
|
||||
// enregistre les routes API
|
||||
const infoRoute = require("./routes/info");
|
||||
const downloadRoute = require("./routes/download");
|
||||
debug("Routes /download et /info installees");
|
||||
app.use("/download", downloadRoute);
|
||||
app.use("/info", infoRoute);
|
||||
|
||||
// route GET / de base
|
||||
app.get("/", (req, res) => {
|
||||
debug("Requete GET / servie");
|
||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||
});
|
||||
|
||||
// fonction pour démarrer le serveur (Promise pour pouvoir await ailleurs)
|
||||
function startServer() {
|
||||
// Fonction pour démarrer le serveur
|
||||
async function startServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.info("Demarrage du serveur Express...");
|
||||
const serverInstance = app.listen(8080, () => {
|
||||
logger.info("Serveur Express pret sur http://localhost:8080");
|
||||
resolve(serverInstance); // succès
|
||||
const server = app.listen(config.applicationPort, () => {
|
||||
logger.info(`Express server ready at http://localhost:${config.applicationPort}`);
|
||||
resolve(server);
|
||||
});
|
||||
serverInstance.on("error", (err) => {
|
||||
logger.error("Erreur serveur Express :", err);
|
||||
|
||||
server.on("error", (err) => {
|
||||
logger.error("Express Server Error :", err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
const gracefulExit = () => {
|
||||
logSessionEnd();
|
||||
server.close(() => {
|
||||
logger.info("Express Server close cleanly.");
|
||||
process.exit();
|
||||
});
|
||||
};
|
||||
|
||||
process.on("SIGINT", gracefulExit);
|
||||
process.on("SIGTERM", gracefulExit);
|
||||
});
|
||||
}
|
||||
|
||||
// gestion de la fermeture propre du process
|
||||
// process.on("exit", () => {
|
||||
// logSessionEnd();
|
||||
// });
|
||||
process.on("SIGINT", () => { // Ctrl+C
|
||||
logSessionEnd();
|
||||
process.exit();
|
||||
});
|
||||
process.on("SIGTERM", () => { // kill
|
||||
logSessionEnd();
|
||||
process.exit();
|
||||
});
|
||||
|
||||
// on exporte la fonction startServer pour que d’autres modules puissent la lancer
|
||||
module.exports = { startServer };
|
||||
62
server/services/download.services.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const { execFile } = require("child_process");
|
||||
const { userYtDlp } = require("../helpers/path");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { logger } = require("../logger");
|
||||
const { buildYtDlpArgs } = require("../helpers/buildArgs");
|
||||
const notify = require("../helpers/notify")
|
||||
|
||||
function fetchDownload(options, listeners, speedListeners) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const outputFolder = options.outputFolder || path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader");
|
||||
fs.mkdirSync(outputFolder, { recursive: true });
|
||||
|
||||
const args = buildYtDlpArgs({ ...options, outputFolder });
|
||||
logger.info(`[yt-dlp args] ${args.join(" ")}`);
|
||||
|
||||
const child = execFile(userYtDlp, args);
|
||||
|
||||
child.on("error", err => reject(new Error(`YT-DLP Error : ${err.message}`)));
|
||||
|
||||
child.on("close", code => {
|
||||
listeners.forEach(fn => fn("done"));
|
||||
if (code === 0) resolve(outputFolder);
|
||||
else reject(new Error(`YT-DLP failed with code : ${code}`));
|
||||
});
|
||||
|
||||
|
||||
child.stdout.on("data", data => {
|
||||
data.toString().split("\n").forEach(line => {
|
||||
if (!line.trim()) return;
|
||||
logger.info(`[yt-dlp] ${line}`);
|
||||
|
||||
/* Barre de Chargement*/
|
||||
if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset"));
|
||||
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
|
||||
if (match) listeners.forEach(fn => fn(parseFloat(match[1])));
|
||||
|
||||
if (line.includes("MiB/s") || line.includes("KiB/s")) {
|
||||
const match = line.match(/\d+(\.\d+)?[KM]?iB\/s/);
|
||||
if (match) {
|
||||
const speed = match[0];
|
||||
speedListeners.forEach(fn => fn(speed));
|
||||
}
|
||||
}
|
||||
|
||||
if (line.match(/ERROR: Could not copy .* cookie database/i)) {
|
||||
notify.notifyCookiesBrowserError();
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
child.stderr.on("data", data => {
|
||||
data.toString().split("\n").forEach(line => line.trim() && logger.error(`[yt-dlp stderr] ${line}`));
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { fetchDownload };
|
||||
51
server/services/info.services.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const { execFile } = require("child_process");
|
||||
const { userYtDlp, denoPath } = require("../helpers/path");
|
||||
const getUserBrowser = require("../helpers/getBrowser");
|
||||
const { logger } = require("../logger");
|
||||
|
||||
function fetchInfo(url) {
|
||||
const args = [
|
||||
"--js-runtimes", `deno:${denoPath}`,
|
||||
"--dump-single-json",
|
||||
"--ignore-errors",
|
||||
"--yes-playlist",
|
||||
"--cookies-from-browser",`${getUserBrowser()}`,
|
||||
"--extractor-args","youtube:player_client=default",
|
||||
"--ignore-no-formats-error",
|
||||
];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
execFile(userYtDlp,[...args, url],
|
||||
{ timeout: 30_000, // 30s, si jamais plus, abandon de la requête
|
||||
maxBuffer: 10 * 1024 * 1024 }, // 10MO de réponse max (Par défaut: 200ko)
|
||||
|
||||
(error, stdout, stderr) => {
|
||||
|
||||
if (stderr) {
|
||||
const lower = stderr.toLowerCase();
|
||||
if (lower.includes("sign in to confirm") || lower.includes("failed to decrypt") || lower.includes("could not copy")) {
|
||||
return reject(new Error(`Unable to extract cookies from ${getUserBrowser()}. Please log in to this browser and try again.`));
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
logger.error(`YT-DLP Error: ${error.message}`);
|
||||
if (stderr) logger.debug(`stderr: ${stderr}`);
|
||||
return reject(new Error("Unable to fetch Info."));
|
||||
}
|
||||
|
||||
if (!stdout) return reject(new Error("No Data Received."));
|
||||
|
||||
try {
|
||||
const data = JSON.parse(stdout);
|
||||
resolve(data);
|
||||
} catch (e) {
|
||||
logger.error(`JSON Parsing Error: ${e.message}`);
|
||||
reject(new Error("JSON unreadable."));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { fetchInfo };
|
||||
45
server/update.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
const { app, Notification } = require("electron");
|
||||
const { logger } = require("./logger");
|
||||
|
||||
function AutoUpdater() {
|
||||
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
logger.info(`New Version Available : ${info.version}`);
|
||||
new Notification({
|
||||
title: "Freedom Loader",
|
||||
body: `New Version Available : ${info.version}. Application will restart`
|
||||
}).show();
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
logger.info(`Update Downloaded : ${info.version}`);
|
||||
new Notification({
|
||||
title: "Freedom Loader",
|
||||
body: `Update ${info.version} downloaded.`
|
||||
}).show();
|
||||
|
||||
setTimeout(() => {
|
||||
autoUpdater.quitAndInstall();
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
autoUpdater.on("error", (err) => {
|
||||
logger.error("Auto Update Error :", err.message);
|
||||
new Notification({
|
||||
title: "Freedom Loader - Update Error",
|
||||
body: err.message
|
||||
}).show();
|
||||
});
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
try {
|
||||
await autoUpdater.checkForUpdates();
|
||||
logger.info("Update check completed");
|
||||
} catch (err) {
|
||||
logger.error("Error during update check :", err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { AutoUpdater };
|
||||