Compare commits
177 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 | ||
|
|
251049c7fe | ||
|
|
5d347967b1 | ||
|
|
d5c506c4e0 | ||
|
|
6718f4133e | ||
|
|
9ff6678730 | ||
|
|
3957450fdb | ||
|
|
25e50a6e7a | ||
|
|
22f935f678 | ||
|
|
58b843630b | ||
|
|
dec3f3272a | ||
|
|
f3252c2548 | ||
|
|
ac28afb17a | ||
|
|
b1a42f44fa | ||
|
|
78eea2fa0f | ||
|
|
4290e6db52 | ||
|
|
0ffc1af218 | ||
|
|
10cefdba19 | ||
|
|
c888946588 | ||
|
|
9026c63979 | ||
|
|
cfc2c5bbaa | ||
|
|
a41da5c4c2 | ||
|
|
b4242e4295 |
8
.gitignore
vendored
@@ -1,9 +1,11 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/downloads
|
/downloads
|
||||||
/dist
|
/dist
|
||||||
/logs
|
|
||||||
/out
|
/out
|
||||||
|
|
||||||
|
/ressources/ffmpeg.exe
|
||||||
|
/ressources/ffprobe.exe
|
||||||
|
/ressources/deno.exe
|
||||||
|
|
||||||
yt-dlp.exe
|
logs/*.json
|
||||||
forge.config.js
|
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/votre-repo/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/votre-utilisateur/votre-repo/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.
|
|
||||||
252
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
|
<div align="center">
|
||||||
|
<a href="https://github.com/MasterAcnolo/Freedom-Loader/releases">
|
||||||
## 🚀 Fonctionnalités
|
<img src="https://img.shields.io/badge/Release-1.3.0-blue?style=for-the-badge">
|
||||||
|
|
||||||
✅ 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**
|
|
||||||
<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éé)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🖼️ Aperçu
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="build/exemple.png" alt="Exemple d'application" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
> Voici un exemple de l’application lancée après avoir saisi une URL.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ℹ️ À propos
|
|
||||||
|
|
||||||
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/votre-repo/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/votre-utilisateur/votre-repo/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>
|
</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>
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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/example-developertools.png" width="20%" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📄 Licence
|
## Roadmap
|
||||||
|
|
||||||
Ce projet est sous licence **MIT** — voir le fichier [LICENSE](./LICENSE) pour plus d’informations.
|
- [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~~
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚠️ Disclaimer
|
## Preview
|
||||||
|
|
||||||
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.
|
<p align="center">
|
||||||
|
<img src="build/apercu1.2.4.png" width="40%" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technologies
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
160
main.js
@@ -1,74 +1,156 @@
|
|||||||
/*
|
const config = require("./config.js");
|
||||||
This file is part of Freedom Loader.
|
const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
|
||||||
|
|
||||||
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 path = require("path");
|
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;
|
let mainWindow;
|
||||||
|
const logsFolderPath = logDir;
|
||||||
|
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
||||||
|
|
||||||
async function createWindow() {
|
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
|
||||||
logger.info("Creation de la fenetre...");
|
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) {
|
if (mainWindow) {
|
||||||
logger.warn("La fenetre existe deja, pas de nouvelle creation");
|
logger.info("New Instance Detected, closing the older...");
|
||||||
|
mainWindow.destroy();
|
||||||
|
mainWindow = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Création fenêtre principale
|
||||||
|
async function createMainWindow() {
|
||||||
|
if (mainWindow) {
|
||||||
|
logger.warn("Window already exists, no new creation!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1000,
|
title: `Freedom Loader ${config.version}`,
|
||||||
height: 700,
|
width: 750,
|
||||||
|
height: 800,
|
||||||
|
minWidth: 750,
|
||||||
|
minHeight: 800,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, "preload.js"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await mainWindow.loadURL("http://localhost:8080");
|
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
|
||||||
logger.info("Fenetre chargee");
|
logger.info("Window Loaded");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Erreur chargement fenetre:", err);
|
logger.error("Window Loading Error:", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow.on("closed", () => {
|
mainWindow.on("closed", () => {
|
||||||
logger.info("Fenetre principale fermee");
|
logger.info("Main Window Closed");
|
||||||
mainWindow = null;
|
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 () => {
|
app.whenReady().then(async () => {
|
||||||
logSessionStart();
|
logSessionStart();
|
||||||
logger.info("App prete, demarrage du serveur Express...");
|
logger.info("App Ready, Server Express starting...");
|
||||||
const expressServer = require("./server/server.js");
|
|
||||||
|
const serverPath = path.join(__dirname, "server", "server.js");
|
||||||
|
const expressServer = require(serverPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await expressServer.startServer();
|
await expressServer.startServer();
|
||||||
logger.info("Serveur Express demarre");
|
logger.info("Express Server Started");
|
||||||
await createWindow();
|
|
||||||
} catch (error) {
|
const { startRPC } = require("./server/discordRPC");
|
||||||
logger.error("Erreur serveur ou fenetre :", error);
|
startRPC();
|
||||||
|
|
||||||
|
await createMainWindow();
|
||||||
|
AutoUpdater(mainWindow);
|
||||||
|
setupMenu();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Window or Server error :", err);
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
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();
|
if (process.platform !== "darwin") app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("before-quit", () => {
|
app.on("before-quit", () => logSessionEnd());
|
||||||
logSessionEnd();
|
|
||||||
});
|
|
||||||
|
|||||||
4672
package-lock.json
generated
71
package.json
@@ -1,56 +1,93 @@
|
|||||||
{
|
{
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
"version": "1.0.0",
|
"version": "1.3.0",
|
||||||
"author": "MasterAcnolo",
|
"author": "MasterAcnolo",
|
||||||
"description": "Freedom Loader, parce que nous le valons bien",
|
"description": "Freedom Loader",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-forge start",
|
"start": "electron --trace-warnings .",
|
||||||
"package": "electron-forge package",
|
|
||||||
"make": "electron-forge make",
|
|
||||||
"dist": "electron-builder"
|
"dist": "electron-builder"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
|
"discord-rpc": "^4.0.1",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
|
"electron-updater": "^6.6.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"webidl-conversions": "^8.0.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0"
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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": "^37.1.0",
|
||||||
"electron-builder": "^26.0.12"
|
"electron-builder": "^26.0.12"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.masteracnolo.freedomloader",
|
"appId": "com.masteracnolo.freedomloader",
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
|
"publish": [
|
||||||
|
{
|
||||||
|
"provider": "github",
|
||||||
|
"owner": "MasterAcnolo",
|
||||||
|
"repo": "Freedom-Loader"
|
||||||
|
}
|
||||||
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"**/*",
|
"**/*",
|
||||||
"node_modules/**/*"
|
"node_modules/**/*",
|
||||||
|
"!logs/*.json",
|
||||||
|
"server/**/*",
|
||||||
|
"server/node_modules/**/*"
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"buildResources": "build"
|
"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": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"icon": "build/installer-icon.ico"
|
"icon": "build/app-icon.ico"
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
"perMachine": true,
|
"perMachine": true,
|
||||||
"allowToChangeInstallationDirectory": true,
|
"allowToChangeInstallationDirectory": true,
|
||||||
"installerIcon": "build/installer-icon.ico",
|
"installerIcon": "build/app-icon.ico",
|
||||||
"uninstallerIcon": "build/uninstaller-icon.ico",
|
"uninstallerIcon": "build/uninstaller-icon.ico",
|
||||||
"createDesktopShortcut": true,
|
"createDesktopShortcut": true,
|
||||||
"createStartMenuShortcut": 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 |
@@ -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,21 +9,23 @@
|
|||||||
font-src https://fonts.gstatic.com;
|
font-src https://fonts.gstatic.com;
|
||||||
img-src 'self' data: https://i.ytimg.com;
|
img-src 'self' data: https://i.ytimg.com;
|
||||||
">
|
">
|
||||||
<title>Freedom Loader</title>
|
<title></title>
|
||||||
|
|
||||||
<!--CSS-->
|
|
||||||
<link rel="stylesheet" href="styles/styles.css">
|
<link rel="stylesheet" href="styles/styles.css">
|
||||||
|
|
||||||
<!--Icons-->
|
|
||||||
<link rel="icon" href="assets/icons/file-audio-solid.svg" type="image/svg+xml">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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">
|
<div class="container">
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1> Freedom Loader </h1>
|
<h1 id="title"> Freedom Loader </h1>
|
||||||
<p> Because why not? </p>
|
<p id="subtitle"> Because why not? </p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form action="/download" method="POST" id="downloadForm">
|
<form action="/download" method="POST" id="downloadForm">
|
||||||
@@ -35,35 +37,55 @@
|
|||||||
<input type="checkbox" name="audioOnly" value="1">
|
<input type="checkbox" name="audioOnly" value="1">
|
||||||
<div class="checkmark"></div>
|
<div class="checkmark"></div>
|
||||||
</label>
|
</label>
|
||||||
<!-- <input type="checkbox" name="audioOnly" value="1"> -->
|
Audio Only (MP3)
|
||||||
Audio seulement (MP3)
|
|
||||||
</label>
|
</label>
|
||||||
<!-- <input type="checkbox" name="audioOnly" value="1"> Audio seulement (MP3) -->
|
|
||||||
<select name="quality">
|
<select name="quality">
|
||||||
<option value="best">Meilleure qualité</option>
|
<option value="best">Best Quality</option>
|
||||||
<option value="worst">Qualité minimale</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>
|
</select>
|
||||||
<button type="submit">Télécharger</button>
|
|
||||||
|
<button type="submit" onclick="startProgress()">Download</button>
|
||||||
</form>
|
</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 id="downloadStatus"></div>
|
||||||
|
|
||||||
<!--Il faut utiliser un formulaire, parce que on doit envoyer a un serveur
|
<div class="infos-container">
|
||||||
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.-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="loaderBox" class="loader-box" style="display:none;">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<span>Fetching info...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="infos-container">
|
|
||||||
<div id="videoInfo" style="margin-top: 20px;"></div>
|
<div id="videoInfo" style="margin-top: 20px;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="script/custompath.js"></script>
|
||||||
<script src="script/downloadstatus.js"></script>
|
<script src="script/downloadstatus.js"></script>
|
||||||
<script src="script/fetchinfo.js"></script>
|
<script src="script/fetchinfo.js"></script>
|
||||||
|
<script src="script/progressBar.js"></script>
|
||||||
|
<script src="script/customthemes.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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");
|
const form = document.getElementById("downloadForm");
|
||||||
|
|
||||||
// Div où on affiche l'état de progression (en cours, erreur, succès)
|
|
||||||
const statusDiv = document.getElementById("downloadStatus");
|
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");
|
const button = form.querySelector("button");
|
||||||
|
|
||||||
// On intercepte la soumission du formulaire
|
|
||||||
form.addEventListener("submit", async (e) => {
|
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 formData = new FormData(form);
|
||||||
const params = new URLSearchParams(formData);
|
const params = new URLSearchParams(formData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Envoi de la requête POST vers /download avec les données du formulaire
|
|
||||||
const res = await fetch("/download", {
|
const res = await fetch("/download", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
body: params,
|
body: params,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Si la réponse n'est pas un succès (code 4xx ou 5xx)
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
statusDiv.textContent = "❌ Erreur pendant le téléchargement.";
|
statusDiv.textContent = res;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// On récupère le texte de la réponse du serveur
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
|
|
||||||
// Affiche le message de fin de téléchargement (par exemple « Téléchargement terminé »)
|
|
||||||
statusDiv.textContent = text;
|
statusDiv.textContent = text;
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
// Gestion d'erreur en cas de problème réseau ou autre
|
statusDiv.textContent = "❌ An Error has Occured.";
|
||||||
statusDiv.textContent = "❌ Une erreur s’est produite.";
|
|
||||||
} finally {
|
} finally {
|
||||||
// Réactive le bouton pour autoriser d'autres soumissions
|
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
|
|
||||||
// Efface le message de status après 5 secondes pour garder l'UI propre
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
statusDiv.textContent = "";
|
statusDiv.textContent = "";
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|||||||
@@ -1,114 +1,180 @@
|
|||||||
/*
|
function formatDate(dateStr) {
|
||||||
This file is part of Freedom Loader.
|
if (!dateStr || dateStr.length !== 8) return "Inconnue";
|
||||||
|
return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`;
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const urlInput = document.getElementById("UrlInput");
|
|
||||||
const infoDiv = document.getElementById("videoInfo");
|
|
||||||
|
|
||||||
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.");
|
|
||||||
infoDiv.innerHTML = "";
|
|
||||||
infoDiv.classList.remove("visible");
|
|
||||||
lastFetchedUrl = "";
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url === lastFetchedUrl) {
|
function formatSize(bytes) {
|
||||||
if (DEBUG) console.log("[DEBUG] Même URL que précédemment, pas de fetch.");
|
return bytes ? (bytes / (1024*1024)).toFixed(2) + " Mo" : "Inconnue";
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastFetchedUrl = url;
|
async function fetchVideoInfo(url) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (DEBUG) console.log("[DEBUG] Envoi requête POST vers /info avec URL:", url);
|
|
||||||
const res = await fetch("/info", {
|
const res = await fetch("/info", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
body: new URLSearchParams({ url }),
|
body: new URLSearchParams({ url }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (DEBUG) console.log("[DEBUG] Réponse status:", res.status);
|
if (!res.ok) return { error: `An Error occured when fetching info` };
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (DEBUG) console.log("[DEBUG] Données reçues:", data);
|
if (!data) return { error: "Data is Missing" };
|
||||||
|
|
||||||
// Vérification minimaliste
|
return data;
|
||||||
if (!data || !data.title || !data.thumbnail) {
|
} catch (e) {
|
||||||
console.warn("[WARNING] Données incomplètes", data);
|
return { error: "Network or JSON Issue" };
|
||||||
infoDiv.innerHTML = "❌ Données incomplètes reçues.";
|
}
|
||||||
infoDiv.classList.remove("visible");
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const urlInput = document.getElementById("UrlInput");
|
||||||
|
const infoDiv = document.getElementById("videoInfo");
|
||||||
|
const loaderBox = document.getElementById("loaderBox");
|
||||||
|
|
||||||
|
let lastFetchedUrl = "";
|
||||||
|
|
||||||
|
urlInput.addEventListener("input", async () => {
|
||||||
|
const url = urlInput.value.trim();
|
||||||
|
|
||||||
|
// Si champ vide -> reset total
|
||||||
|
if (!url || url.length < 2) {
|
||||||
|
infoDiv.innerHTML = "";
|
||||||
|
infoDiv.classList.remove("visible", "playlist-mode");
|
||||||
|
lastFetchedUrl = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const minutes = Math.floor(data.duration / 60);
|
if (url === lastFetchedUrl) return;
|
||||||
const seconds = data.duration % 60;
|
lastFetchedUrl = url;
|
||||||
const durationStr = `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
|
|
||||||
|
|
||||||
const date = data.upload_date || "";
|
loaderBox.style.display = "flex";
|
||||||
const readableDate = date
|
const data = await fetchVideoInfo(url);
|
||||||
? `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}`
|
loaderBox.style.display = "none";
|
||||||
: "Inconnue";
|
|
||||||
|
|
||||||
const sizeMB = data.filesize_approx
|
// Gestion des erreurs
|
||||||
? (data.filesize_approx / (1024 * 1024)).toFixed(2) + " Mo"
|
if (data.error) {
|
||||||
: "Inconnue";
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
const categories = data.categories
|
// ---------- PLAYLIST ----------
|
||||||
? data.categories.join(", ")
|
if (data.type === "playlist") {
|
||||||
: "Non spécifiées";
|
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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gestion du bouton copier
|
||||||
|
listDiv.addEventListener("click", (event) => {
|
||||||
|
if (event.target.classList.contains("copy-btn")) {
|
||||||
|
const btn = event.target;
|
||||||
|
if (btn.disabled) return;
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
const url = btn.dataset.url;
|
||||||
|
navigator.clipboard.writeText(url)
|
||||||
|
.then(() => {
|
||||||
|
const original = btn.textContent;
|
||||||
|
|
||||||
|
btn.style.opacity = 0;
|
||||||
|
btn.style.transform = "scale(0.7)";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = "✅";
|
||||||
|
btn.style.opacity = 1;
|
||||||
|
btn.style.transform = "scale(1)";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.style.opacity = 0;
|
||||||
|
btn.style.transform = "scale(0.7)";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = original;
|
||||||
|
btn.style.opacity = 1;
|
||||||
|
btn.style.transform = "scale(1)";
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
}, 300);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
const original = btn.textContent;
|
||||||
|
btn.textContent = "❌";
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = original;
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
infoDiv.classList.add("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 = `
|
infoDiv.innerHTML = `
|
||||||
<h3>${data.title}</h3>
|
<h3>${data.title}</h3>
|
||||||
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Durée :</strong> ${durationStr}</li>
|
<li><strong>Duration :</strong> ${durationStr}</li>
|
||||||
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
|
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
|
||||||
<li><strong>Date d’upload :</strong> ${readableDate}</li>
|
<li><strong>Upload Date :</strong> ${readableDate}</li>
|
||||||
<li><strong>Vues :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
|
<li><strong>Views :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
|
||||||
<li><strong>Likes :</strong> ${data.like_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>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" rel="noopener noreferrer">${data.channel_url}</a></li>
|
<li><strong>Channel :</strong> <a href="${data.channel_url}" target="_blank">${data.channel_url}</a></li>
|
||||||
<li><strong>Taille estimée :</strong> ${sizeMB}</li>
|
<li><strong>Estimed Size :</strong> ${sizeStr}</li>
|
||||||
<li><strong>Catégories :</strong> ${categories}</li>
|
<li><strong>Category :</strong> ${categories}</li>
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
infoDiv.classList.add("visible");
|
|
||||||
|
|
||||||
if (DEBUG) console.log("[DEBUG] Info affichée avec succès.");
|
infoDiv.classList.add("visible");
|
||||||
} catch (e) {
|
|
||||||
console.error("[CRITICAL] Erreur lors de la récupération:", e);
|
|
||||||
infoDiv.innerHTML = "❌ Erreur réseau ou JSON.";
|
|
||||||
infoDiv.classList.remove("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"
|
||||||
|
};
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
/* From Uiverse.io by bociKond */
|
|
||||||
/* Hide the default checkbox */
|
|
||||||
.checkbox-mp3 input {
|
.checkbox-mp3 input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -16,52 +14,47 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Create a custom checkbox */
|
|
||||||
.checkmark {
|
.checkmark {
|
||||||
--clr: #0B6E4F;
|
--clr: var(--checkbox-bg-checked);
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 1.3em;
|
height: 1.3em;
|
||||||
width: 1.3em;
|
width: 1.3em;
|
||||||
background-color: #ccc;
|
background-color: var(--checkbox-bg-default);
|
||||||
border-radius: 50%;
|
border-radius: .5rem;
|
||||||
transition: 300ms;
|
transition: background-color 300ms, border-radius 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* When the checkbox is checked, add a blue background */
|
|
||||||
.checkbox-mp3 input:checked ~ .checkmark {
|
.checkbox-mp3 input:checked ~ .checkmark {
|
||||||
background-color: var(--clr);
|
background-color: var(--clr);
|
||||||
border-radius: .5rem;
|
border-radius: .5rem;
|
||||||
animation: pulse 500ms ease-in-out;
|
animation: pulse 500ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Create the checkmark/indicator (hidden when not checked) */
|
|
||||||
.checkmark:after {
|
.checkmark:after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Show the checkmark when checked */
|
|
||||||
.checkbox-mp3 input:checked ~ .checkmark:after {
|
.checkbox-mp3 input:checked ~ .checkmark:after {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style the checkmark/indicator */
|
|
||||||
.container .checkmark:after {
|
.container .checkmark:after {
|
||||||
left: 0.45em;
|
left: 0.45em;
|
||||||
top: 0.25em;
|
top: 0.25em;
|
||||||
width: 0.25em;
|
width: 0.25em;
|
||||||
height: 0.5em;
|
height: 0.5em;
|
||||||
border: solid #E0E0E2;
|
border: solid var(--checkbox-checkmark-border-color);
|
||||||
border-width: 0 0.15em 0.15em 0;
|
border-width: 0 0.15em 0.15em 0;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% {
|
0% {
|
||||||
box-shadow: 0 0 0 #0B6E4F90;
|
box-shadow: 0 0 0 var(--checkbox-pulse-color);
|
||||||
rotate: 20deg;
|
rotate: 20deg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,11 +63,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
75% {
|
75% {
|
||||||
box-shadow: 0 0 0 10px #0B6E4F60;
|
box-shadow: 0 0 0 10px var(--checkbox-pulse-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 0 13px #0B6E4F30;
|
box-shadow: 0 0 0 13px var(--checkbox-pulse-color);
|
||||||
rotate: 0;
|
rotate: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
/* height: 10000px; */
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
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 {
|
form {
|
||||||
background-color: #fff;
|
background-color: var(--form-bg-color);
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
@@ -13,8 +13,10 @@ form input[type="text"],
|
|||||||
form input[type="url"],
|
form input[type="url"],
|
||||||
form select {
|
form select {
|
||||||
padding: 0.75em;
|
padding: 0.75em;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid var(--form-input-border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
background-color: var(--form-input-bg-color);
|
||||||
|
color: var(--form-input-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
form input[type="checkbox"] {
|
form input[type="checkbox"] {
|
||||||
@@ -22,8 +24,8 @@ form input[type="checkbox"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form button {
|
form button {
|
||||||
background-color: #007bff;
|
background-color: var(--form-button-bg-color);
|
||||||
color: #fff;
|
color: var(--form-button-text-color);
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.75em;
|
padding: 0.75em;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -32,7 +34,7 @@ form button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form button:hover {
|
form button:hover {
|
||||||
background-color: #0056b3;
|
background-color: var(--form-button-bg-hover-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
form#downloadForm {
|
form#downloadForm {
|
||||||
@@ -48,26 +50,27 @@ form#downloadForm {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
color: var(--form-input-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#UrlInput {
|
#UrlInput {
|
||||||
padding: 0.75em 1em;
|
padding: 0.75em 1em;
|
||||||
border-color: #ccc;
|
border-color: var(--form-input-border-color);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
background-color: #f9f9f9;
|
background-color: var(--form-input-bg-color);
|
||||||
color: #333;
|
color: var(--form-input-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#UrlInput:focus {
|
#UrlInput:focus {
|
||||||
border-color: #0056b3;
|
border-color: var(--form-input-border-focus-color);
|
||||||
box-shadow: 0 0 5px rgba(0,123,255,0.4);
|
box-shadow: 0 0 5px rgba(0,123,255,0.4);
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: #fff;
|
background-color: var(--form-input-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#UrlInput::placeholder {
|
#UrlInput::placeholder {
|
||||||
color: #aaa;
|
color: var(--form-input-placeholder-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
padding: 1.5em 2em;
|
padding: 1.5em 2em;
|
||||||
background-color: #fff;
|
background-color: var(--infos-box-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||||||
color: #222;
|
color: var(--video-info-text-color);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -21,13 +21,12 @@
|
|||||||
#videoInfo.visible {
|
#videoInfo.visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
max-height: 1500px;
|
max-height: 1500px;
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#videoInfo h3 {
|
#videoInfo h3 {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
margin-bottom: 0.6em;
|
margin-bottom: 0.6em;
|
||||||
color: #007bff;
|
color: var(--video-info-heading-color);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
@@ -51,15 +50,15 @@
|
|||||||
#videoInfo ul li {
|
#videoInfo ul li {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #444;
|
color: var(--video-info-list-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#videoInfo ul li strong {
|
#videoInfo ul li strong {
|
||||||
color: #000;
|
color: var(--video-info-list-strong-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#videoInfo ul li a {
|
#videoInfo ul li a {
|
||||||
color: #007bff;
|
color: var(--video-info-link-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +67,81 @@
|
|||||||
text-decoration: underline;
|
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 */
|
/* Responsive */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
#videoInfo {
|
#videoInfo {
|
||||||
|
|||||||
@@ -1,17 +1,34 @@
|
|||||||
|
/* Layout */
|
||||||
@import url("layout/form.css");
|
@import url("layout/form.css");
|
||||||
@import url("layout/container.css");
|
@import url("layout/container.css");
|
||||||
@import url("layout/ctr-infos.css");
|
|
||||||
@import url("layout/header.css");
|
@import url("layout/header.css");
|
||||||
@import url("layout/videoinfo.css");
|
@import url("layout/videoinfo.css");
|
||||||
@import url("button/checkbox.css");
|
|
||||||
|
/* Components */
|
||||||
|
@import url("components/checkbox.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');
|
@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{
|
html,body{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
/* outline: 2px solid red; */
|
/*outline: 2px solid red; /*<-- Utile pour le debug ça */
|
||||||
font-family: "Poppins", sans-serif;
|
font-family: "Poppins", sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -19,14 +36,10 @@ html,body{
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
/* background-color: #f5f5f5; */
|
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%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
color: #333;
|
color: var(--default-text-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -34,26 +47,21 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header p{
|
||||||
/* #videoInfo {
|
color: var(--subtitle-color);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#videoInfo.visible {
|
.checkbox-inline{
|
||||||
opacity: 1;
|
color: var(--mp3-text-color);
|
||||||
max-height: 1000px;
|
}
|
||||||
} */
|
|
||||||
|
body,
|
||||||
|
.container,
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
label {
|
||||||
|
transition: background-color 0.2s ease, color 0.5s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
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")
|
||||||
36
public/styles/variables.css
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
:root {
|
||||||
|
/* Couleurs générales */
|
||||||
|
--background-color: #001224;
|
||||||
|
--default-text-color: #eee;
|
||||||
|
--subtitle-color: #007bff;
|
||||||
|
--mp3-text-color: #000000;
|
||||||
|
|
||||||
|
/* 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 };
|
||||||
@@ -1,76 +1,52 @@
|
|||||||
/*
|
|
||||||
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 { createLogger, format, transports } = require("winston");
|
||||||
const DailyRotateFile = require("winston-daily-rotate-file");
|
const DailyRotateFile = require("winston-daily-rotate-file");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const os = require("os");
|
||||||
|
const config = require("../config")
|
||||||
|
|
||||||
const logDir = path.join(__dirname, "../logs");
|
// Dossier de logs Windows
|
||||||
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir);
|
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
|
||||||
|
|
||||||
function getSessionStartLine() {
|
// Création du dossier "logs" si nécessaire
|
||||||
const now = new Date().toISOString();
|
fs.mkdirSync(logDir, { recursive: true });
|
||||||
return `--- Démarrage de la session : ${now} ---`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSessionEndLine() {
|
const logFormat = format.combine(
|
||||||
const now = new Date().toISOString();
|
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
return `--- Fin de la session : ${now} ---`;
|
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
||||||
}
|
);
|
||||||
|
|
||||||
|
// Configuration du logger
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
level: "info",
|
level: "info",
|
||||||
format: format.combine(
|
format: logFormat,
|
||||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
||||||
format.printf(({ timestamp, level, message }) =>
|
|
||||||
`${timestamp} | ${level.toUpperCase()} | ${message}`
|
|
||||||
)
|
|
||||||
),
|
|
||||||
transports: [
|
transports: [
|
||||||
new DailyRotateFile({
|
new DailyRotateFile({
|
||||||
dirname: logDir,
|
dirname: logDir,
|
||||||
filename: "combined-%DATE%.log",
|
filename: "LOGS-%DATE%.log",
|
||||||
datePattern: "YYYY-MM-DD",
|
datePattern: "YYYY-MM-DD",
|
||||||
zippedArchive: false, // tu peux mettre true si tu veux compresser
|
zippedArchive: false,
|
||||||
maxFiles: "14d", // conserve les logs des 14 derniers jours
|
maxFiles: "7d",
|
||||||
format: format.combine(
|
format: logFormat,
|
||||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
||||||
format.printf(({ timestamp, level, message }) =>
|
|
||||||
`${timestamp} | ${level.toUpperCase()} | ${message}`
|
|
||||||
)
|
|
||||||
),
|
|
||||||
options: { flags: "a" },
|
options: { flags: "a" },
|
||||||
}),
|
}),
|
||||||
new transports.Console({
|
new transports.Console({
|
||||||
format: format.combine(
|
format: logFormat,
|
||||||
format.colorize(),
|
|
||||||
format.timestamp({ format: "HH:mm:ss" }),
|
|
||||||
format.printf(({ timestamp, level, message }) =>
|
|
||||||
`${timestamp} | ${level} | ${message}`
|
|
||||||
)
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getSessionStartLine() {
|
||||||
|
return `--- Starting session: ${new Date().toISOString()} ---`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSessionEndLine() {
|
||||||
|
return `--- Ending session: ${new Date().toISOString()} ---`;
|
||||||
|
}
|
||||||
|
|
||||||
function logSessionStart() {
|
function logSessionStart() {
|
||||||
logger.info(getSessionStartLine());
|
logger.info(getSessionStartLine());
|
||||||
|
logger.info(`Application Version: ${config.version}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function logSessionEnd() {
|
function logSessionEnd() {
|
||||||
@@ -81,4 +57,5 @@ module.exports = {
|
|||||||
logger,
|
logger,
|
||||||
logSessionStart,
|
logSessionStart,
|
||||||
logSessionEnd,
|
logSessionEnd,
|
||||||
|
logDir,
|
||||||
};
|
};
|
||||||
@@ -1,105 +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");
|
|
||||||
const router = express.Router();
|
|
||||||
const { execFile } = require("child_process");
|
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const logger = require("../logger").logger; // importe ton logger Winston
|
|
||||||
|
|
||||||
const ytDlpPath = path.join(__dirname, '../../yt-dlp.exe');
|
|
||||||
|
|
||||||
router.post("/", (req, res) => {
|
|
||||||
try {
|
|
||||||
const options = {
|
|
||||||
url: req.body.url,
|
|
||||||
audioOnly: req.body.audioOnly === "1",
|
|
||||||
quality: req.body.quality || "best",
|
|
||||||
subtitles: req.body.subs === "1",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!options.url) {
|
|
||||||
logger.warn("Requête POST /download sans URL");
|
|
||||||
return res.status(400).send("❌ URL manquante !");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer le chemin Freedom Loader Output depuis app.locals
|
|
||||||
const outputFolder = req.app.locals.outputFolder;
|
|
||||||
|
|
||||||
if (!fs.existsSync(outputFolder)) {
|
|
||||||
fs.mkdirSync(outputFolder, { recursive: true });
|
|
||||||
logger.info("Dossier Freedom Loader Output créé à la volée dans Téléchargements.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construire le template de sortie
|
|
||||||
const outputTemplate = path.join(outputFolder, "%(title)s.%(ext)s");
|
|
||||||
|
|
||||||
// Construire les arguments pour execFile
|
|
||||||
const args = [];
|
|
||||||
|
|
||||||
if (options.audioOnly) {
|
|
||||||
args.push("--extract-audio", "--audio-format", "mp3");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.subtitles) {
|
|
||||||
args.push("--write-subs", "--sub-lang", "en");
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push("-f", options.quality);
|
|
||||||
args.push("-o", outputTemplate);
|
|
||||||
args.push(options.url);
|
|
||||||
|
|
||||||
logger.info(`Commande yt-dlp : ${ytDlpPath} ${args.join(" ")}`);
|
|
||||||
|
|
||||||
const child = execFile(ytDlpPath, args);
|
|
||||||
|
|
||||||
child.stdout.on("data", (data) => {
|
|
||||||
data.toString().split("\n").forEach(line => {
|
|
||||||
if(line.trim()) logger.info(`[yt-dlp stdout] ${line.trim()}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stderr.on("data", (data) => {
|
|
||||||
data.toString().split("\n").forEach(line => {
|
|
||||||
if(line.trim()) logger.error(`[yt-dlp stderr] ${line.trim()}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("error", (err) => {
|
|
||||||
logger.error(`Erreur lancement yt-dlp : ${err.message}`);
|
|
||||||
res.status(500).send(`❌ Erreur lors de l'exécution : ${err.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", (code) => {
|
|
||||||
logger.info(`yt-dlp terminé avec code de sortie : ${code}`);
|
|
||||||
if(code === 0) {
|
|
||||||
res.send("✅ Téléchargement terminé !");
|
|
||||||
} else {
|
|
||||||
res.status(500).send(`❌ yt-dlp a échoué avec le code : ${code}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Erreur serveur dans /download : ${err.message}`);
|
|
||||||
res.status(500).send(`Erreur serveur : ${err.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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,47 +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");
|
|
||||||
const router = express.Router();
|
|
||||||
const { execFile } = require("child_process");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const ytDlpPath = path.join(__dirname, "../../yt-dlp.exe");
|
|
||||||
|
|
||||||
router.post("/", (req, res) => {
|
|
||||||
const url = req.body.url;
|
|
||||||
if (!url) return res.status(400).send("❌ URL manquante");
|
|
||||||
|
|
||||||
execFile(ytDlpPath, ["--dump-json", url], (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
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 {
|
|
||||||
const info = JSON.parse(stdout);
|
|
||||||
res.json(info);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Erreur lors du parsing JSON :", e);
|
|
||||||
res.status(500).send("❌ JSON illisible.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
||||||
103
server/server.js
@@ -1,93 +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");
|
const express = require("express");
|
||||||
const { exec } = require("child_process");
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const debug = require("debug")("freedom-loader:server");
|
|
||||||
const { logger, logSessionStart, logSessionEnd } = require("./logger");
|
const { logger, logSessionStart, logSessionEnd } = require("./logger");
|
||||||
|
const config = require("../config");
|
||||||
|
const { execFile } = require("child_process");
|
||||||
|
const { userYtDlp } = require("./helpers/path");
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// // Lancement d'une nouvelle session de log
|
app.use(express.json());
|
||||||
// logSessionStart();
|
|
||||||
|
|
||||||
// dossier de sortie
|
// Dossier de téléchargement
|
||||||
const downloadsPath = path.join(process.env.USERPROFILE, "Downloads");
|
const outputFolder = path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader");
|
||||||
const outputFolder = path.join(downloadsPath, "Freedom Loader Output");
|
|
||||||
|
|
||||||
if (!fs.existsSync(outputFolder)) {
|
// Création du dossier si nécessaire
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(outputFolder, { recursive: true });
|
fs.mkdirSync(outputFolder, { recursive: true });
|
||||||
logger.info("Dossier Freedom Loader Output cree dans Telechargements.");
|
logger.info(`Freedom Loader folder ready in ${outputFolder}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Impossible de creer le dossier :", err);
|
logger.error("Unable to create folder:", err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
logger.info("Dossier Freedom Loader Output deja existant.");
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Middlewares
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(express.static(path.join(__dirname, "../public")));
|
||||||
|
|
||||||
const staticPath = path.join(__dirname, "../public");
|
// Routes
|
||||||
debug("Serveur statique sur", staticPath);
|
app.use("/download", require("./routes/download.route"));
|
||||||
app.use(express.static(staticPath));
|
app.use("/info", require("./routes/info.route"));
|
||||||
|
|
||||||
// routes
|
|
||||||
const infoRoute = require("./routes/info");
|
|
||||||
const downloadRoute = require("./routes/download");
|
|
||||||
debug("Routes /download et /info installees");
|
|
||||||
app.use("/download", downloadRoute);
|
|
||||||
app.use("/info", infoRoute);
|
|
||||||
|
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
debug("Requete GET / servie");
|
|
||||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
function startServer() {
|
// Fonction pour démarrer le serveur
|
||||||
|
async function startServer() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
logger.info("Demarrage du serveur Express...");
|
const server = app.listen(config.applicationPort, () => {
|
||||||
const serverInstance = app.listen(8080, () => {
|
logger.info(`Express server ready at http://localhost:${config.applicationPort}`);
|
||||||
logger.info("Serveur Express pret sur http://localhost:8080");
|
resolve(server);
|
||||||
resolve(serverInstance);
|
|
||||||
});
|
});
|
||||||
serverInstance.on("error", (err) => {
|
|
||||||
logger.error("Erreur serveur Express :", err);
|
server.on("error", (err) => {
|
||||||
|
logger.error("Express Server Error :", err);
|
||||||
reject(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 propre de la fin de session à la fermeture du process
|
|
||||||
// process.on("exit", () => {
|
|
||||||
// logSessionEnd();
|
|
||||||
// });
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
logSessionEnd();
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
logSessionEnd();
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = { startServer };
|
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 };
|
||||||