Compare commits
230 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac0b4a0750 | ||
|
|
29ca2ea496 | ||
|
|
2a3905bdfb | ||
|
|
d3755ab57d | ||
|
|
9824e107fe | ||
|
|
e437722937 | ||
|
|
fd8c4ae33c | ||
|
|
967144bd55 | ||
|
|
cd8eb05020 | ||
|
|
590060e504 | ||
|
|
26c7d4d2ee | ||
|
|
caa987fa42 | ||
|
|
ccb090e4b2 | ||
|
|
8d070ed10d | ||
|
|
343dbb26a2 | ||
|
|
fef4f6de36 | ||
|
|
1a61972750 | ||
|
|
45ae5d7f98 | ||
|
|
fecb123d0e | ||
|
|
5c62ed081a | ||
|
|
8f0f31041d | ||
|
|
0e48ab3b4c | ||
|
|
d4c33bcf2d | ||
|
|
2f105f5145 | ||
|
|
b7a193b062 | ||
|
|
65364d3095 | ||
|
|
c356e2e30d | ||
|
|
e45810b54c | ||
|
|
55db30febc | ||
|
|
163faec221 | ||
|
|
5398fed928 | ||
|
|
f959171db5 | ||
|
|
01b6bcd02c | ||
|
|
0ff4715b35 | ||
|
|
745a73a5b0 | ||
|
|
a69114edce | ||
|
|
489a4766fa | ||
|
|
adcee62386 | ||
|
|
b867d819b7 | ||
|
|
31d8b20a76 | ||
|
|
42ae0e7204 | ||
|
|
43869420c0 | ||
|
|
6c9d296fbd | ||
|
|
008428f10a | ||
|
|
ae7d338474 | ||
|
|
12ff25cc4b | ||
|
|
f31767e7e7 | ||
|
|
7c4f2841e1 | ||
|
|
c451ddd042 | ||
|
|
da6311055f | ||
|
|
945efb2059 | ||
|
|
ab2881f8e9 | ||
|
|
f23811fff4 | ||
|
|
19ab77501b | ||
|
|
37eeed583a | ||
|
|
caf08efb90 | ||
|
|
7ff998e4df | ||
|
|
0c1e8fe899 | ||
|
|
4158e8c6e3 | ||
|
|
653319e408 | ||
|
|
0a4f14fba7 | ||
|
|
fb4571fc51 | ||
|
|
c11fd40681 | ||
|
|
cbfde8b761 | ||
|
|
c774821cc4 | ||
|
|
2ed15a32bc | ||
|
|
c252853603 | ||
|
|
cfdddec073 | ||
|
|
97a63ab791 | ||
|
|
a0e00341e1 | ||
|
|
26d42491ce | ||
|
|
d678ba2f12 | ||
|
|
55b400eb05 | ||
|
|
0a6313b1f3 | ||
|
|
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 | ||
|
|
dc50a44682 | ||
|
|
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 |
35
.github/ISSUE_TEMPLATE/-bug--issues-template-.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: "[BUG] Issues Template "
|
||||
about: Report something that doesn't work fine
|
||||
title: "[BUG] Name of the bug / issues"
|
||||
labels: bug
|
||||
assignees: MasterAcnolo
|
||||
|
||||
---
|
||||
|
||||
## Check Before Reporting
|
||||
Make sure this bug doesn't already exist in the issues.
|
||||
|
||||
## Describe the Bug
|
||||
A clear and concise description of the problem.
|
||||
|
||||
## Steps to Reproduce
|
||||
1. Go to…
|
||||
2. Click on…
|
||||
3. See the error…
|
||||
|
||||
## Expected Behavior
|
||||
What you expected the program to do.
|
||||
|
||||
## Logs
|
||||
Add logs if possible. Available at `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-20xx-xx-xx.log`.
|
||||
|
||||
## App Version
|
||||
State the version of the app you are using.
|
||||
|
||||
## Environment
|
||||
- Version: [e.g. 1.2.4]
|
||||
- Browser / Other: [e.g. Chrome, Node.js 20]
|
||||
- Any others information that can be usefull for me
|
||||
|
||||
## Screenshots (if applicable)
|
||||
22
.github/ISSUE_TEMPLATE/-feat--feature-template.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: "[FEAT] Feature Template"
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEAT] Name of the features"
|
||||
labels: enhancement
|
||||
assignees: MasterAcnolo
|
||||
|
||||
---
|
||||
|
||||
## Check Before Requesting
|
||||
Make sure this feature doesn't already exist in the issues.
|
||||
|
||||
## Feature Description
|
||||
Describe the feature you want to add or improve.
|
||||
|
||||
## Why is this Feature Useful?
|
||||
Explain the benefit for users or the project.
|
||||
|
||||
## Examples / Context
|
||||
Provide an example or use case if possible.
|
||||
|
||||
## Screenshots / Mockups (if applicable)
|
||||
29
.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
## Description
|
||||
|
||||
Describe the feature or improvement you are adding.
|
||||
Include why it's useful and what problem it solves.
|
||||
|
||||
## Type of Change
|
||||
|
||||
- [ ] New feature
|
||||
- [ ] Improvement / Refactor
|
||||
- [ ] Documentation update
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Code tested locally
|
||||
- [ ] No linting errors
|
||||
- [ ] Added or updated tests if relevant
|
||||
- [ ] Updated documentation if relevant
|
||||
|
||||
## Verification Steps
|
||||
|
||||
Explain how reviewers can test the new feature or improvement.
|
||||
|
||||
## App Version
|
||||
|
||||
State the version of Freedom Loader you are targeting.
|
||||
|
||||
## Notes / Additional Info
|
||||
|
||||
Any context, screenshots, or logs that can help reviewing the PR.
|
||||
25
.github/PULL_REQUEST_TEMPLATE/version.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Freedom Loader - x.y.z
|
||||
|
||||
## Changelog
|
||||
|
||||
- List all changes made in this release (features, bug fixes, improvements)
|
||||
- Use bullet points, keep it concise
|
||||
- Example:
|
||||
- `YT-DLP` Will now update when the application is launched
|
||||
- Improved UI / UX
|
||||
|
||||
## Bug Reports / Issues
|
||||
|
||||
- Please report any bugs in the GitHub Issues section
|
||||
|
||||
## Next Release Roadmap (non-exhaustive)
|
||||
|
||||
- List upcoming features or improvements
|
||||
- Example:
|
||||
- More format options
|
||||
- Subtitle support
|
||||
- Language selection
|
||||
- Parallel downloads
|
||||
- Skip sponsored parts automatically
|
||||
|
||||
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/[previous-version]...[new-version]
|
||||
7
.gitignore
vendored
@@ -1,8 +1,11 @@
|
||||
/node_modules
|
||||
/downloads
|
||||
/dist
|
||||
/logs
|
||||
/out
|
||||
|
||||
/ressources/ffmpeg.exe
|
||||
/ressources/ffprobe.exe
|
||||
/ressources/deno.exe
|
||||
|
||||
yt-dlp.exe
|
||||
logs/*.json
|
||||
logs/*.log
|
||||
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
masteracnolo25@gmail.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
66
CONTRIBUTING.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Contributing to Freedom Loader
|
||||
|
||||
Welcome! Thanks for being interested in Freedom Loader. Your contributions-bug reports, feature ideas, or code improvements-are always appreciated. Every contribution makes Freedom Loader better 💪
|
||||
|
||||
---
|
||||
|
||||
## 1. Check Before Contributing
|
||||
- Make sure your issue, bug, or feature request doesn't already exist.
|
||||
- Search issues and pull requests before creating a new one.
|
||||
- Follow the [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
---
|
||||
|
||||
## 2. Report an Issue
|
||||
Found a bug, weird behavior, or have an idea? Open an [**issue**](https://github.com/MasterAcnolo/Freedom-Loader/issues) using the **Bug Report template**.
|
||||
Make sure to include:
|
||||
- Steps to reproduce the issue
|
||||
- App version and environment (OS, Node, Browser, etc.)
|
||||
- Logs if possible: `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-20xx-xx-xx.log`
|
||||
- Screenshots if relevant
|
||||
|
||||
---
|
||||
|
||||
## 3. Submit a Pull Request
|
||||
To fix a bug, add a feature, or improve docs:
|
||||
1. Fork the repository
|
||||
2. Create a branch: `git checkout -b feature/my-awesome-feature`
|
||||
3. Make your changes
|
||||
4. Add tests or verification steps
|
||||
5. Test everything locally
|
||||
6. Open a PR to the `main` branch
|
||||
|
||||
**Tips for PRs:**
|
||||
- Keep titles and descriptions clear and concise
|
||||
- Document what you change and why
|
||||
- Follow existing code style (camelCase for variables/functions)
|
||||
- Small commits focused on a single purpose are easier to review
|
||||
|
||||
---
|
||||
|
||||
## 4. Improve the Documentation
|
||||
Docs are important: README, wiki, guides… If something is unclear, improve it!
|
||||
Small contributions like fixing typos or adding examples are always welcome.
|
||||
|
||||
---
|
||||
|
||||
## 5. Testing & Verification
|
||||
- Test new features thoroughly
|
||||
- Ensure bug fixes actually fix the reported issue
|
||||
- Include any relevant logs or screenshots
|
||||
|
||||
---
|
||||
|
||||
## 6. Review & Merge
|
||||
- After submitting a PR, it will be reviewed by maintainers
|
||||
- Be open to feedback and changes
|
||||
- We try to respond quickly, but sometimes have other priorities
|
||||
|
||||
---
|
||||
|
||||
## 7. Code of Conduct
|
||||
Please contribute respectfully and constructively. Everyone is welcome as long as discussions remain polite and productive.
|
||||
|
||||
---
|
||||
|
||||
Thanks to Zakaria for the website icon. Check him out here: [IG Link](https://www.instagram.com/designmark_studio/) 🔥
|
||||
103
README-EN.md
@@ -1,103 +0,0 @@
|
||||
# Freedom Loader
|
||||
|
||||
 
|
||||
|
||||
> This project aims to enable free, open-source, and unrestricted downloading of Video/Audio content. Eventually, it will allow users to choose
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
✅ Video Download (MP4)
|
||||
✅ Audio Download (MP3)
|
||||
✅ Detailed Logs (Console + Logs)
|
||||
✅ Automatic Fetch of Information
|
||||
✅ Playlist Download
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Installation
|
||||
|
||||
To install the application, simply download the installer from the releases section. Take the latest version and run it.
|
||||
|
||||
---
|
||||
|
||||
## ❓ Frequently Asked Questions
|
||||
|
||||
### _Where are my downloaded files located?_
|
||||
|
||||
In your computer's Downloads folder (**C:\Users\YOUR NAME\Downloads**), inside a folder named **Freedom Loader Output**
|
||||
<br>
|
||||
|
||||
### _After installing and running the program, I get an error_
|
||||
|
||||
Try restarting the application, checking the logs, or opening the browser's developer console by pressing `CTRL + SHIFT + I` or by going to `View > Toggle Developer Tools`
|
||||
|
||||
<!--  -->
|
||||
<p align="center">
|
||||
<img src="build/example-developertools.png" alt="Example Developer Tools" />
|
||||
</p>
|
||||
|
||||
## 🏗️ Upcoming Features (Roadmap)
|
||||
|
||||
- [ ] More format options
|
||||
- [ ] Subtitle support
|
||||
- [ ] Improved UI / UX
|
||||
- [ ] Language selection
|
||||
- [ ] Download a specific part of the video
|
||||
- [ ] Rename output file
|
||||
- [ ] Extract / Split video using native chapters
|
||||
- [ ] Parallel downloads
|
||||
- [ ] Skip sponsored parts of videos
|
||||
- [ ] Custom output path selection (default **C:\Users\Username\Downloads** with a **Freedom Loader Output** folder created)
|
||||
|
||||
---
|
||||
|
||||
## 🖼️ Preview
|
||||
|
||||
<p align="center">
|
||||
<img src="build/exemple.png" alt="Example Image" />
|
||||
</p>
|
||||
|
||||
> Here is an example of the app after launch and entering a URL
|
||||
|
||||
---
|
||||
|
||||
## ℹ️ About
|
||||
|
||||
This project was built using [](https://github.com/yt-dlp/yt-dlp)
|
||||
Thanks to them for providing a free and open-source API.
|
||||
|
||||
This project is open-source and welcomes your contributions.
|
||||
Here is how to get involved:
|
||||
|
||||
- **🪲 Bug Reporting**
|
||||
Please use the [GitHub issues system](https://github.com/MasterAcnolo/Freedom-Loader/issues) to report any bugs you encounter.
|
||||
Try to provide clear reproduction steps and logs if possible. Logs are located in the installation folder of the program. Example path:
|
||||
`C:\Users\[YOUR NAME]\Desktop\Freedom Loader\resources\app\logs`
|
||||
The log file will be named something like `combined-20xx-xx-xx.log`
|
||||
<br>
|
||||
- **🛠️ Contribution**
|
||||
Pull Requests are welcome! Please fork the repo and submit your PR after ensuring you meet the contribution guidelines (tests, style, etc.).
|
||||
<br>
|
||||
- **💡 Feature Suggestions**
|
||||
You can propose new ideas via a [feature request issue](https://github.com/MasterAcnolo/Freedom-Loader/issues).
|
||||
Any clear and detailed proposal will be reviewed.
|
||||
<br>
|
||||
- **💰 Support the Project**
|
||||
<p>
|
||||
If you'd like to support me, you can donate via
|
||||
<a href="https://paypal.me/axelnicolas25">
|
||||
<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white" alt="PayPal">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the **MIT License** — see the [LICENSE](./LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Disclaimer
|
||||
|
||||
I have obviously used AI assistance for some parts of this project. Use AI sparingly and always be aware of what you are doing.
|
||||
209
README.md
@@ -1,103 +1,150 @@
|
||||
# Freedom Loader
|
||||
<div align="center">
|
||||
<a href="https://masteracnolo.github.io/FreedomLoader/"><img
|
||||
src="./build/banner.png"
|
||||
alt="Banner"
|
||||
style="width: 50%;;"/></a>
|
||||
</div>
|
||||
|
||||
 
|
||||
|
||||
> Ce projet a pour but de permettre le téléchargement de contenu Vidéo/Audio de manière gratuite, open source et libre. Il permettra à terme de choisir
|
||||
|
||||
## 🚀 Fonctionnalités
|
||||
|
||||
✅ Téléchargement Vidéo (MP4)
|
||||
✅ Téléchargement Audio (MP3)
|
||||
✅ Logs détaillés (console + fichiers logs)
|
||||
✅ Récupération automatique des informations
|
||||
✅ Téléchargement des playlists
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Installation
|
||||
|
||||
Pour installer l'application, il vous suffit de télécharger l'installateur dans la section releases, de prendre la dernière version et de l'exécuter.
|
||||
|
||||
---
|
||||
|
||||
## ❓ Questions fréquentes
|
||||
|
||||
### _Où se situent mes fichiers une fois téléchargés ?_
|
||||
|
||||
Dans le dossier Téléchargements (**C:\Users\VOTRE NOM\Downloads**) de votre ordinateur, dans un dossier nommé **Freedom Loader Output**
|
||||
<div align="center">
|
||||
<a href="https://github.com/MasterAcnolo/Freedom-Loader/releases"><img src="https://img.shields.io/badge/Release-1.4.0-blue?style=for-the-badge"></a>
|
||||
<a href="https://masteracnolo.github.io/FreedomLoader/index.html"><img src="https://img.shields.io/badge/Freedom%20Loader-Open%20Page-111111?style=for-the-badge&logo=terminal&logoColor=white"></a>
|
||||
<a href="https://www.firefox.com/fr/?utm_campaign=SET_DEFAULT_BROWSER"><img src="https://img.shields.io/badge/Require Firefox-E66000?style=for-the-badge&logo=Firefox-Browser&logoColor=white"></a>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
### _Une fois le programme installé et lancé, j'ai une erreur_
|
||||
|
||||
Essayez de relancer l'application, de consulter les logs ou d'ouvrir la console de développement du navigateur en faisant le raccourci `CTRL + SHIFT + I` ou via `View > Toggle Developer Tools`.
|
||||
|
||||
<!--  -->
|
||||
<p align="center">
|
||||
<img src="build/example-developertools.png" alt="Exemple Developer Tools" />
|
||||
</p>
|
||||
|
||||
## 🏗️ Fonctionnalités à venir (Roadmap)
|
||||
|
||||
- [ ] Plus de choix de formats
|
||||
- [ ] Support des sous-titres
|
||||
- [ ] Meilleure UI / UX
|
||||
- [ ] Choix des langues
|
||||
- [ ] Téléchargement d’une partie de la vidéo
|
||||
- [ ] Renommage du fichier de sortie
|
||||
- [ ] Extraction / découpage de la vidéo via chapitrage natif
|
||||
- [ ] Téléchargements parallèles
|
||||
- [ ] Suppression automatique des parties sponsorisées (skip sponsor)
|
||||
- [ ] Choix personnalisé du chemin de sortie (par défaut **C:\Users\Utilisateur\Downloads**, où un dossier **Freedom Loader Output** sera créé)
|
||||
Freedom Loader is a **Windows desktop** application built with **Electron**.
|
||||
It provides a simple and reliable way to download video or audio content with metadata and advanced options, without ads or questionable third-party services.
|
||||
The goal is to offer a clean, user-friendly, and transparent download experience. It's important for me to make media content downloading accessible to people who love music or videos and don’t always have an internet connection.
|
||||
|
||||
---
|
||||
|
||||
## 🖼️ Aperçu
|
||||
## Features
|
||||
|
||||
- Video download with metadata (MP4)
|
||||
- Audio-only download with thumbnail and tags (MP3)
|
||||
- Full playlist support and better display in UI
|
||||
- Automatic metadata retrieval (title, duration, author, etc.)
|
||||
- Detailed logs (console + rotating files for 7 days)
|
||||
- Progress Bar for download and network speed indicator
|
||||
- Better UX when fetching info
|
||||
- Custom output path with persistence
|
||||
- Automatic dependency updates (YT-DLP)
|
||||
- Automatic Application Update
|
||||
- Multiple UI themes
|
||||
- Simple, responsive interface
|
||||
- Notifications on download completion (click to open folder)
|
||||
- Discord Rich Presence
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Download the latest installer from the **Releases** page and run it (Defender could stop you but click on "Run Anyway" and it's good).
|
||||
Actually, **Firefox Browser** is the only browser supported for get cookies. You should install it. Follow the tutorial in the wiki page if you need some help, Firefox Section. [Link](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html)
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Where are my downloaded files stored?
|
||||
|
||||
In your **Downloads** folder, inside **Freedom Loader**.
|
||||
Example path:
|
||||
`C:\Users\[YOUR NAME]\Downloads\Freedom Loader`
|
||||
|
||||
### I get an error after launching the app
|
||||
|
||||
Try restarting the application, checking the logs, or opening the developer console:
|
||||
`CTRL + SHIFT + I` -> “Toggle Developer Tools”.
|
||||
|
||||
<p align="center">
|
||||
<img src="build/exemple.png" alt="Exemple d'application" />
|
||||
<img src="build/example-developertools.png" width="20%" />
|
||||
</p>
|
||||
|
||||
> Voici un exemple de l’application lancée après avoir saisi une URL.
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] ~~Website for Freedom Loader~~
|
||||
- [ ] More format options
|
||||
- [ ] Linux version
|
||||
- [x] ~~Auto-updating YT-DLP~~
|
||||
- [x] ~~Automatic project updates~~
|
||||
- [x] ~~Better download status~~
|
||||
- [ ] Better website support (currently only YouTube is fully supported, other site could work but i can't prove it)
|
||||
- [ ] Subtitle support
|
||||
- [x] ~~UI/UX improvements~~
|
||||
- [ ] Language selection
|
||||
- [ ] Download specific parts of a video
|
||||
- [ ] Custom file naming
|
||||
- [ ] Parallel downloads
|
||||
- [ ] Automatic sponsor skipping
|
||||
- [x] ~~Extract/split video using native chapters~~
|
||||
- [x] ~~Custom output path selection~~
|
||||
|
||||
---
|
||||
|
||||
## ℹ️ À propos
|
||||
## Preview
|
||||
|
||||
Ce projet a été réalisé en utilisant [](https://github.com/yt-dlp/yt-dlp)
|
||||
Merci à eux de nous permettre d’utiliser leur API gratuitement et en open source.
|
||||
|
||||
Ce projet est open source et accueille volontiers vos contributions.
|
||||
Voici comment participer :
|
||||
|
||||
- **🪲 Signalement de bugs**
|
||||
Merci d’utiliser le [système d’issues GitHub](https://github.com/MasterAcnolo/Freedom-Loader/issues) pour remonter les bugs rencontrés.
|
||||
Essayez de fournir une reproduction claire et les logs si possible. Les logs se trouvent dans le dossier d’installation du programme. Exemple de chemin :
|
||||
`C:\Users\[VOTRE NOM]\Desktop\Freedom Loader\resources\app\logs`
|
||||
Le fichier aura un nom du type `combined-20xx-xx-xx.log`.
|
||||
<br>
|
||||
- **🛠️ Contribution**
|
||||
Les Pull Requests sont les bienvenues ! Merci de forker le repo et de soumettre votre PR après avoir vérifié le respect des guidelines (tests, style, etc.).
|
||||
<br>
|
||||
- **💡 Suggestions de fonctionnalités**
|
||||
Vous pouvez proposer de nouvelles idées via une [issue de type feature request](https://github.com/MasterAcnolo/Freedom-Loader/issues).
|
||||
Toute proposition claire et détaillée sera étudiée.
|
||||
<br>
|
||||
- **💰 Soutenir le projet**
|
||||
<p>
|
||||
Si vous souhaitez m’encourager, vous pouvez faire un don sur
|
||||
<a href="https://paypal.me/axelnicolas25">
|
||||
<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white" alt="PayPal">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="build/apercu1.2.4.png" width="40%" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📄 Licence
|
||||
## Technologies
|
||||
|
||||
Ce projet est sous licence **MIT** — voir le fichier [LICENSE](./LICENSE) pour plus d’informations.
|
||||
This project uses:
|
||||
|
||||
<p>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Disclaimer
|
||||
## Contributing
|
||||
|
||||
J’ai bien évidemment eu recours à l’IA pour m’aider sur certaines parties du projet. Utilisez l’IA avec parcimonie et soyez toujours conscient de ce que vous faites.
|
||||
Contributions are welcome.
|
||||
|
||||
### Bug Reports
|
||||
|
||||
Use the GitHub Issues system and include reproduction steps and logs when possible.
|
||||
Logs can be found here:
|
||||
`C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-20xx-xx-xx.log`
|
||||
|
||||
### Feature Requests
|
||||
|
||||
Open a feature request issue with a clear description and use cases.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
Fork the repository, make your changes, and submit a PR once everything is tested and clean.
|
||||
|
||||
---
|
||||
|
||||
## Support the Project
|
||||
|
||||
If you'd like to support development:
|
||||
|
||||
<a href="https://paypal.me/axelnicolas25">
|
||||
<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white" />
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Freedom Loader is released under the **GNU General Public License v3.0 (GPLv3)**.
|
||||
You are free to use, modify, and redistribute the software under the terms of this license.
|
||||
|
||||
See the full license in the [LICENSE](./LICENSE) file.
|
||||
|
||||
---
|
||||
|
||||
## Note
|
||||
|
||||
AI assistance was used occasionally to speed up development, but all code and decisions were reviewed and validated manually.
|
||||
|
||||
40
SECURITY.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We actively maintain security updates for the following versions (Since the 1.3.0 the application auto-update, patch will be apply easily):
|
||||
|
||||
| Version | Supported |
|
||||
| -------- | ----------------- |
|
||||
| 1.3.x | :white_check_mark: |
|
||||
| 1.2.x | :white_check_mark: |
|
||||
| 1.1.x | :white_check_mark:|
|
||||
| <1.1 | :white_check_mark: |
|
||||
|
||||
> Only versions marked with ✅ receive security updates. Older versions may be vulnerable.
|
||||
|
||||
---
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in Freedom Loader, **do not create a public issue**. Report it privately:
|
||||
|
||||
- Email: **masteracnolo25@gmail.com**
|
||||
- Include:
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce it
|
||||
- App version and environment (OS, Node, Browser if relevant)
|
||||
- Logs or screenshots if possible (`C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-20xx-xx-xx.log`)
|
||||
|
||||
We will respond as quickly as possible, typically within **48 hours**, and keep you updated on the fix.
|
||||
|
||||
We may acknowledge responsible disclosures in release notes unless you request anonymity.
|
||||
|
||||
---
|
||||
|
||||
## What Happens Next
|
||||
|
||||
- We will confirm receipt of your report
|
||||
- Investigate and reproduce the issue
|
||||
- Deploy a fix and release an update
|
||||
- Notify you when the fix is live
|
||||
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.bmp
Normal file
|
After Width: | Height: | Size: 151 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 |
26
config.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const packageJson = require("./package.json");
|
||||
const { app } = require("electron");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const localMode = !app.isPackaged;
|
||||
|
||||
const featuresPath = localMode ? path.join(__dirname, "./config/config.json") : path.join(path.join(process.resourcesPath,"config/" ,"config.json"));
|
||||
|
||||
let features = {};
|
||||
|
||||
function loadFeatures() {
|
||||
const raw = fs.readFileSync(featuresPath, "utf-8");
|
||||
features = JSON.parse(raw);
|
||||
return features;
|
||||
}
|
||||
|
||||
const configFeatures = loadFeatures();
|
||||
|
||||
module.exports = {
|
||||
version: packageJson.version,
|
||||
applicationPort: "8787",
|
||||
localMode,
|
||||
DiscordRPCID: "1410934537051181146",
|
||||
configFeatures
|
||||
}
|
||||
15
config/config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"autoUpdate": true,
|
||||
"discordRPC": true,
|
||||
"customTopBar": true,
|
||||
"autoCheckInfo": true,
|
||||
"addThumbnail": true,
|
||||
"addMetadata": true,
|
||||
"verboseLogs": false,
|
||||
"autoDownloadPlaylist": true,
|
||||
"customCodec": "h264",
|
||||
"logSystem": true,
|
||||
"outputTitleCheck": true,
|
||||
"downloadSystem": true,
|
||||
"notifySystem": true
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
|
||||
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
|
||||
|
||||
module.exports = {
|
||||
packagerConfig: {
|
||||
asar: true,
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers: [
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-zip',
|
||||
platforms: ['darwin'],
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-deb',
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-rpm',
|
||||
config: {},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
name: '@electron-forge/plugin-auto-unpack-natives',
|
||||
config: {},
|
||||
},
|
||||
// Fuses are used to enable/disable various Electron functionality
|
||||
// at package time, before code signing the application
|
||||
new FusesPlugin({
|
||||
version: FuseVersion.V1,
|
||||
[FuseV1Options.RunAsNode]: false,
|
||||
[FuseV1Options.EnableCookieEncryption]: true,
|
||||
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
||||
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||
}),
|
||||
],
|
||||
};
|
||||
270
main.js
@@ -1,79 +1,267 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
|
||||
Freedom Loader is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
|
||||
Freedom Loader is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const { app, BrowserWindow } = require("electron");
|
||||
const config = require("./config.js");
|
||||
const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
|
||||
const path = require("path");
|
||||
const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
|
||||
const os = require("os");
|
||||
const fs = require("fs");
|
||||
|
||||
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
||||
const { AutoUpdater } = require("./server/update.js");
|
||||
const { configFeatures } = require("./config.js");
|
||||
const { startRPC } = require("./server/discordRPC");
|
||||
|
||||
let mainWindow;
|
||||
const logsFolderPath = logDir;
|
||||
|
||||
async function createWindow() {
|
||||
logger.info("Creation de la fenetre...");
|
||||
|
||||
const basePath = config.localMode
|
||||
? path.join(__dirname )
|
||||
: path.join(path.dirname(process.execPath), "resources");
|
||||
|
||||
const configFolderPath = path.join(basePath, "config" ,"config.json");
|
||||
|
||||
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
||||
|
||||
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
|
||||
app.disableHardwareAcceleration();
|
||||
|
||||
ipcMain.handle("version", () => config.version);
|
||||
|
||||
// Gestion single instance
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
|
||||
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
|
||||
function checkNativeDependencies() {
|
||||
const deps = [
|
||||
{ name: "yt-dlp.exe", path: path.join(process.resourcesPath, "binaries","yt-dlp.exe") },
|
||||
{ name: "ffmpeg.exe", path: path.join(process.resourcesPath, "binaries", "ffmpeg.exe") },
|
||||
{ name: "ffprobe.exe", path: path.join(process.resourcesPath, "binaries", "ffprobe.exe") },
|
||||
{ name: "deno.exe", path: path.join(process.resourcesPath, "binaries", "deno.exe") },
|
||||
];
|
||||
const missing = deps.filter(dep => !fs.existsSync(dep.path));
|
||||
let errorMsg = "";
|
||||
if (missing.length > 0) {
|
||||
const missingList = missing.map(dep => dep.name).join(", ");
|
||||
logger.error(`Missing dependencies: ${missingList}`);
|
||||
errorMsg += `The following files are missing in the 'ressources' folder:\n${missingList}`;
|
||||
}
|
||||
if (errorMsg) {
|
||||
app.whenReady().then(() => {
|
||||
dialog.showErrorBox(
|
||||
"Missing dependencies",
|
||||
`${errorMsg}\n\nThe application will now exit. Try to reinstall`
|
||||
);
|
||||
app.quit();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!config.localMode){
|
||||
if (!gotLock) {
|
||||
// Une instance existe déjà -> fermer l'ancienne et continuer la nouvelle
|
||||
// Ici la nouvelle instance continue normalement
|
||||
} else {
|
||||
if (!checkNativeDependencies()) {
|
||||
// Arrêt déjà géré dans la fonction
|
||||
} else {
|
||||
app.on("second-instance", () => {
|
||||
// La vieille instance se ferme
|
||||
if (mainWindow) {
|
||||
logger.info("New Instance Detected, closing the older...");
|
||||
mainWindow.destroy();
|
||||
mainWindow = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Création fenêtre principale
|
||||
async function createMainWindow() {
|
||||
if (mainWindow) {
|
||||
logger.warn("La fenetre existe deja, pas de nouvelle creation");
|
||||
logger.warn("Window already exists, no new creation!");
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
title: "Freedom Loader",
|
||||
title: `Freedom Loader ${config.version}`,
|
||||
width: 750,
|
||||
height: 800,
|
||||
minWidth: 750,
|
||||
minHeight: 800,
|
||||
frame: !configFeatures.customTopBar,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
// titleBarStyle: 'hidden',
|
||||
});
|
||||
|
||||
try {
|
||||
await mainWindow.loadURL("http://localhost:8080");
|
||||
logger.info("Fenetre chargee");
|
||||
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
|
||||
logger.info("Window Loaded");
|
||||
} catch (err) {
|
||||
logger.error("Erreur chargement fenetre:", err);
|
||||
logger.error("Window Loading Error:", err);
|
||||
}
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
logger.info("Fenetre principale fermee");
|
||||
logger.info("Main Window Closed");
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
function validateDownloadPath(userPath) {
|
||||
const userHome = os.homedir(); // C:\Users\<User>
|
||||
|
||||
if (!userPath) return path.join(userHome, "Downloads", "Freedom Loader");
|
||||
|
||||
// Résolution canonique et suivi des symlinks
|
||||
const resolved = fs.realpathSync(path.resolve(userPath));
|
||||
const normalizedHome = path.resolve(userHome) + path.sep;
|
||||
|
||||
if (!resolved.startsWith(normalizedHome)) {
|
||||
throw new Error("Chemin non autorisé : uniquement les sous-dossiers du dossier utilisateur sont permis !");
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
|
||||
// IPC
|
||||
ipcMain.handle("select-download-folder", async () => {
|
||||
try {
|
||||
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
const validatedPath = validateDownloadPath(result.filePaths[0]);
|
||||
logger.info(`Folder Checked and Valid : ${validatedPath}`);
|
||||
return validatedPath;
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
logger.error(`An Error Occured when validating folder : ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("validate-download-path", (event, userPath) => {
|
||||
return validateDownloadPath(userPath);
|
||||
});
|
||||
|
||||
|
||||
ipcMain.handle("get-default-download-path", () => defaultDownloadPath);
|
||||
|
||||
ipcMain.on("set-progress", (event, percent) => {
|
||||
if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1
|
||||
});
|
||||
|
||||
// Topbar window controls
|
||||
ipcMain.on("window-minimize", () => {
|
||||
if (mainWindow) mainWindow.minimize();
|
||||
});
|
||||
ipcMain.on("window-maximize", () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
}
|
||||
});
|
||||
ipcMain.on("window-close", () => {
|
||||
if (mainWindow) mainWindow.close();
|
||||
});
|
||||
|
||||
// Topbar custom actions
|
||||
ipcMain.on("open-devtools", () => {
|
||||
if (mainWindow) mainWindow.webContents.openDevTools({ mode: 'detach' });
|
||||
});
|
||||
ipcMain.on("open-logs", () => {
|
||||
if (logsFolderPath) shell.openPath(logsFolderPath);
|
||||
});
|
||||
ipcMain.on("open-website", () => {
|
||||
shell.openExternal("https://masteracnolo.github.io/FreedomLoader/index.html");
|
||||
});
|
||||
ipcMain.on("open-wiki", () => {
|
||||
shell.openExternal("https://masteracnolo.github.io/FreedomLoader/pages/wiki.html");
|
||||
});
|
||||
ipcMain.on("open-config", () => {
|
||||
if (configFolderPath) shell.openPath(configFolderPath);
|
||||
});
|
||||
|
||||
|
||||
// App ready
|
||||
app.whenReady().then(async () => {
|
||||
logSessionStart();
|
||||
logger.info("App prete, demarrage du serveur Express...");
|
||||
const expressServer = require("./server/server.js");
|
||||
logSessionStart();
|
||||
logger.info("App Ready, Server Express starting...");
|
||||
|
||||
const serverPath = path.join(__dirname, "server", "server.js")
|
||||
|
||||
const expressServer = require(serverPath);
|
||||
|
||||
try {
|
||||
await expressServer.startServer();
|
||||
logger.info("Serveur Express demarre");
|
||||
await createWindow();
|
||||
} catch (error) {
|
||||
logger.error("Erreur serveur ou fenetre :", error);
|
||||
logger.info("Express Server Started");
|
||||
|
||||
ipcMain.handle("features", () => {
|
||||
return configFeatures;
|
||||
});
|
||||
|
||||
const featureWhitelist = new Set([
|
||||
"autoUpdate",
|
||||
"discordRPC",
|
||||
"customTopBar",
|
||||
"autoCheckInfo",
|
||||
"addThumbnail",
|
||||
"addMetadata",
|
||||
"verboseLogs",
|
||||
"autoDownloadPlaylist",
|
||||
"customCodec"
|
||||
]);
|
||||
|
||||
|
||||
ipcMain.handle("set-feature", (event, { key, value }) => {
|
||||
try {
|
||||
if (!featureWhitelist.has(key)) {
|
||||
logger.warn(`Rejected feature (not whitelisted): ${key}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// optionnel mais propre
|
||||
if (configFeatures[key] === value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
configFeatures[key] = value;
|
||||
|
||||
fs.writeFileSync(
|
||||
configFolderPath,
|
||||
JSON.stringify(configFeatures, null, 2),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
logger.info(`Feature updated: ${key} = ${value}`);
|
||||
return true;
|
||||
|
||||
} catch (err) {
|
||||
logger.error(`set-feature failed (${key}): ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
configFeatures.discordRPC ? startRPC() : "";
|
||||
|
||||
await createMainWindow();
|
||||
configFeatures.autoUpdate ? AutoUpdater(mainWindow) : ""; // Auto Update
|
||||
|
||||
} catch (err) {
|
||||
logger.error("Window or Server error :", err);
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
logger.info("Toutes fenetres fermees, quitte l'app");
|
||||
logger.info("All Window Closed, shuting down app");
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
|
||||
app.on("before-quit", () => {
|
||||
logSessionEnd();
|
||||
});
|
||||
app.on("before-quit", () => logSessionEnd());
|
||||
|
||||
5089
package-lock.json
generated
79
package.json
@@ -1,57 +1,98 @@
|
||||
{
|
||||
"name": "freedom-loader",
|
||||
"productName": "Freedom Loader",
|
||||
"version": "1.0.1",
|
||||
"version": "1.4.0",
|
||||
"author": "MasterAcnolo",
|
||||
"description": "Freedom Loader, parce que nous le valons bien",
|
||||
"description": "Freedom Loader",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"dist": "electron-builder"
|
||||
"start": "electron --trace-warnings .",
|
||||
"build": "electron-builder",
|
||||
"update": "npm update"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"debug": "^4.4.1",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"discord-rpc": "^4.0.1",
|
||||
"electron-updater": "^6.6.2",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"webidl-conversions": "^8.0.0",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.8.1",
|
||||
"@electron-forge/maker-deb": "^7.8.1",
|
||||
"@electron-forge/maker-rpm": "^7.8.1",
|
||||
"@electron-forge/maker-squirrel": "^7.8.1",
|
||||
"@electron-forge/maker-zip": "^7.8.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
|
||||
"@electron-forge/plugin-fuses": "^7.8.1",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"electron": "^37.1.0",
|
||||
"electron-builder": "^26.0.12"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.masteracnolo.freedomloader",
|
||||
"productName": "Freedom Loader",
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"owner": "MasterAcnolo",
|
||||
"repo": "Freedom-Loader"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"**/*",
|
||||
"node_modules/**/*"
|
||||
"node_modules/**/*",
|
||||
"!logs/*.json"
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "build"
|
||||
},
|
||||
"asar": false,
|
||||
"asar": true,
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "build/confirm-icon.png",
|
||||
"to": "confirm-icon.png"
|
||||
},
|
||||
{
|
||||
"from": "build/app-icon.ico",
|
||||
"to": "app-icon.ico"
|
||||
},
|
||||
{
|
||||
"from": "build/banner.bmp",
|
||||
"to": "banner.bmp"
|
||||
},
|
||||
{
|
||||
"from": "build/error.png",
|
||||
"to": "error.png"
|
||||
},
|
||||
{
|
||||
"from": "ressources/yt-dlp.exe",
|
||||
"to": "binaries/yt-dlp.exe"
|
||||
},
|
||||
{
|
||||
"from": "ressources/ffmpeg.exe",
|
||||
"to": "binaries/ffmpeg.exe"
|
||||
},
|
||||
{
|
||||
"from": "ressources/ffprobe.exe",
|
||||
"to": "binaries/ffprobe.exe"
|
||||
},
|
||||
{
|
||||
"from": "ressources/deno.exe",
|
||||
"to": "binaries/deno.exe"
|
||||
},
|
||||
{
|
||||
"from": "config/config.json",
|
||||
"to": "config/config.json"
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "build/installer-icon.ico"
|
||||
"icon": "build/app-icon.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": true,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"installerIcon": "build/installer-icon.ico",
|
||||
"installerIcon": "build/app-icon.ico",
|
||||
"uninstallerIcon": "build/uninstaller-icon.ico",
|
||||
"installerSidebar": "build/banner.bmp",
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "Freedom Loader"
|
||||
|
||||
23
preload.js
Normal file
@@ -0,0 +1,23 @@
|
||||
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),
|
||||
getFeatures: () => ipcRenderer.invoke("features"),
|
||||
setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
|
||||
getVersion: () => ipcRenderer.invoke("version"),
|
||||
getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path)
|
||||
});
|
||||
|
||||
// Contrôles de fenêtre et outils custom pour la topbar
|
||||
contextBridge.exposeInMainWorld("topbarAPI", {
|
||||
minimize: () => ipcRenderer.send("window-minimize"),
|
||||
maximize: () => ipcRenderer.send("window-maximize"),
|
||||
close: () => ipcRenderer.send("window-close"),
|
||||
openDevTools: () => ipcRenderer.send("open-devtools"),
|
||||
openLogs: () => ipcRenderer.send("open-logs"),
|
||||
openWebsite: () => ipcRenderer.send("open-website"),
|
||||
openWiki: () => ipcRenderer.send("open-wiki"),
|
||||
openConfig: () => ipcRenderer.send("open-config")
|
||||
});
|
||||
|
Before Width: | Height: | Size: 15 KiB |
1
public/assets/icon/close.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128 128C92.7 128 64 156.7 64 192L64 448C64 483.3 92.7 512 128 512L512 512C547.3 512 576 483.3 576 448L576 192C576 156.7 547.3 128 512 128L128 128zM231 231C240.4 221.6 255.6 221.6 264.9 231L319.9 286L374.9 231C384.3 221.6 399.5 221.6 408.8 231C418.1 240.4 418.2 255.6 408.8 264.9L353.8 319.9L408.8 374.9C418.2 384.3 418.2 399.5 408.8 408.8C399.4 418.1 384.2 418.2 374.9 408.8L319.9 353.8L264.9 408.8C255.5 418.2 240.3 418.2 231 408.8C221.7 399.4 221.6 384.2 231 374.9L286 319.9L231 264.9C221.6 255.5 221.6 240.3 231 231z"/></svg>
|
||||
|
After Width: | Height: | Size: 751 B |
1
public/assets/icon/maximize.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128 128C92.7 128 64 156.7 64 192L64 448C64 483.3 92.7 512 128 512L512 512C547.3 512 576 483.3 576 448L576 192C576 156.7 547.3 128 512 128L128 128zM152 192L488 192C501.3 192 512 202.7 512 216C512 229.3 501.3 240 488 240L152 240C138.7 240 128 229.3 128 216C128 202.7 138.7 192 152 192z"/></svg>
|
||||
|
After Width: | Height: | Size: 515 B |
1
public/assets/icon/minimize.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 480C64 462.3 78.3 448 96 448L544 448C561.7 448 576 462.3 576 480C576 497.7 561.7 512 544 512L96 512C78.3 512 64 497.7 64 480z"/></svg>
|
||||
|
After Width: | Height: | Size: 360 B |
|
Before Width: | Height: | Size: 97 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" transform="scale(64)" fill="#1B1F23"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 968 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zm2 226.3c37.1 22.4 62 63.1 62 109.7s-24.9 87.3-62 109.7c-7.6 4.6-17.4 2.1-22-5.4s-2.1-17.4 5.4-22C269.4 401.5 288 370.9 288 336s-18.6-65.5-46.5-82.3c-7.6-4.6-10-14.4-5.4-22s14.4-10 22-5.4zm-91.9 30.9c6 2.5 9.9 8.3 9.9 14.8l0 128c0 6.5-3.9 12.3-9.9 14.8s-12.9 1.1-17.4-3.5L113.4 376 80 376c-8.8 0-16-7.2-16-16l0-48c0-8.8 7.2-16 16-16l33.4 0 35.3-35.3c4.6-4.6 11.5-5.9 17.4-3.5zm51 34.9c6.6-5.9 16.7-5.3 22.6 1.3C249.8 304.6 256 319.6 256 336s-6.2 31.4-16.3 42.7c-5.9 6.6-16 7.1-22.6 1.3s-7.1-16-1.3-22.6c5.1-5.7 8.1-13.1 8.1-21.3s-3.1-15.7-8.1-21.3c-5.9-6.6-5.3-16.7 1.3-22.6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 955 B |
BIN
public/assets/images/Chirac.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
public/assets/images/Drift.jpg
Normal file
|
After Width: | Height: | Size: 967 KiB |
BIN
public/assets/images/NF.jpg
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
public/assets/images/Songbird.webp
Normal file
|
After Width: | Height: | Size: 620 KiB |
BIN
public/assets/images/Spicy.png
Normal file
|
After Width: | Height: | Size: 5.3 MiB |
BIN
public/assets/logo/Freedom Loader White.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/assets/logo/Freedom Loader.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/assets/logo/FreedomLoader2.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
@@ -9,66 +9,268 @@
|
||||
font-src https://fonts.gstatic.com;
|
||||
img-src 'self' data: https://i.ytimg.com;
|
||||
">
|
||||
<title>Freedom Loader</title>
|
||||
<title></title>
|
||||
|
||||
<!--CSS-->
|
||||
<link rel="stylesheet" href="styles/styles.css">
|
||||
<link rel="stylesheet" href="styles/layout/topbar.css">
|
||||
|
||||
<!--Icons-->
|
||||
<link rel="icon" href="assets/icons/file-audio-solid.svg" type="image/svg+xml">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<header>
|
||||
<h1> Freedom Loader </h1>
|
||||
<p> Because why not? </p>
|
||||
</header>
|
||||
|
||||
<form action="/download" method="POST" id="downloadForm">
|
||||
<input name="url" placeholder="URL" id="UrlInput">
|
||||
|
||||
<label class="checkbox-inline">
|
||||
|
||||
<label class="checkbox-mp3">
|
||||
<input type="checkbox" name="audioOnly" value="1">
|
||||
<div class="checkmark"></div>
|
||||
</label>
|
||||
<!-- <input type="checkbox" name="audioOnly" value="1"> -->
|
||||
Audio seulement (MP3)
|
||||
</label>
|
||||
<!-- <input type="checkbox" name="audioOnly" value="1"> Audio seulement (MP3) -->
|
||||
<select name="quality">
|
||||
<option value="best">Meilleure qualité</option>
|
||||
<option value="medium">Qualité moyenne</option>
|
||||
<option value="worst">Qualité minimale</option>
|
||||
<option value="1080">1080p</option>
|
||||
<option value="720">720p</option>
|
||||
<option value="480">480p</option>
|
||||
</select>
|
||||
|
||||
<button type="submit">Télécharger</button>
|
||||
</form>
|
||||
|
||||
<div id="downloadStatus"></div>
|
||||
|
||||
<!--Il faut utiliser un formulaire, parce que on doit envoyer a un serveur
|
||||
les données à traiter. Pour cela on définit une route vers /download, ca va
|
||||
être l'espèce de "canal" attribué a ça. Ca va faire que si jamais on veut ensuite recup
|
||||
les infos du form, il faut aller sur ce canal précis (/download). La méthode POST dit
|
||||
que l'on envoie des données.-->
|
||||
|
||||
|
||||
|
||||
<div class="topbar" id="topbar">
|
||||
<div class="custom-controls">
|
||||
<button id="devtools-btn" title="Dev Tools">Tools</button>
|
||||
<button id="logs-btn" title="Logs">Logs</button>
|
||||
<button id="website-btn" title="Website">Website</button>
|
||||
<button id="wiki-btn" title="Wiki">Wiki</button>
|
||||
</div>
|
||||
<div class="window-controls">
|
||||
<button id="minimize-btn" title="Réduire"><img src="assets/icon/minimize.svg"></button>
|
||||
<button id="maximize-btn" title="Maximiser"><img src="assets/icon/maximize.svg"></button>
|
||||
<button id="close-btn" title="Fermer"><img src="assets/icon/close.svg"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="settings-button" id="settings-button">
|
||||
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><title>ionicons-v5-q</title><path d="M262.29,192.31a64,64,0,1,0,57.4,57.4A64.13,64.13,0,0,0,262.29,192.31ZM416.39,256a154.34,154.34,0,0,1-1.53,20.79l45.21,35.46A10.81,10.81,0,0,1,462.52,326l-42.77,74a10.81,10.81,0,0,1-13.14,4.59l-44.9-18.08a16.11,16.11,0,0,0-15.17,1.75A164.48,164.48,0,0,1,325,400.8a15.94,15.94,0,0,0-8.82,12.14l-6.73,47.89A11.08,11.08,0,0,1,298.77,470H213.23a11.11,11.11,0,0,1-10.69-8.87l-6.72-47.82a16.07,16.07,0,0,0-9-12.22,155.3,155.3,0,0,1-21.46-12.57,16,16,0,0,0-15.11-1.71l-44.89,18.07a10.81,10.81,0,0,1-13.14-4.58l-42.77-74a10.8,10.8,0,0,1,2.45-13.75l38.21-30a16.05,16.05,0,0,0,6-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16,16,0,0,0-6.07-13.94l-38.19-30A10.81,10.81,0,0,1,49.48,186l42.77-74a10.81,10.81,0,0,1,13.14-4.59l44.9,18.08a16.11,16.11,0,0,0,15.17-1.75A164.48,164.48,0,0,1,187,111.2a15.94,15.94,0,0,0,8.82-12.14l6.73-47.89A11.08,11.08,0,0,1,213.23,42h85.54a11.11,11.11,0,0,1,10.69,8.87l6.72,47.82a16.07,16.07,0,0,0,9,12.22,155.3,155.3,0,0,1,21.46,12.57,16,16,0,0,0,15.11,1.71l44.89-18.07a10.81,10.81,0,0,1,13.14,4.58l42.77,74a10.8,10.8,0,0,1-2.45,13.75l-38.21,30a16.05,16.05,0,0,0-6.05,14.08C416.17,247.67,416.39,251.83,416.39,256Z" style="fill:none;stroke:var(--settings-button-text-color);stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>
|
||||
</button>
|
||||
|
||||
|
||||
<div class="settings-panel" id="settings-panel">
|
||||
<div class="settings-modal">
|
||||
|
||||
<button class="close-btn" onclick="closePanel()">✕</button>
|
||||
|
||||
<div class="settings-title">
|
||||
<h2>SETTINGS</h2>
|
||||
<p style="font-size: 0.8em;">Changes are saved automatically. <br> Somes changes could need a restart</p>
|
||||
</div>
|
||||
|
||||
<!-- Basic Settings visibles -->
|
||||
<div class="settings-section">
|
||||
<h3>Basic Settings</h3>
|
||||
|
||||
<div class="setting-item" id="theme" style="margin-left: 32px;">
|
||||
<div class="setting-info">
|
||||
<div style="display: flex;">
|
||||
<span class="setting-title">Thème</span>
|
||||
|
||||
<select id="themeSelect" data-key="theme" aria-label="Choisir le thème">
|
||||
|
||||
</select>
|
||||
</div>
|
||||
<small>Choose the theme of the application</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="discordRPC" id="discordRPC">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Discord RPC</span>
|
||||
<small>Enable/disable Discord Rich Presence for your app.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="autoDownloadPlaylist" id="autoDownloadPlaylist">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Download Playlist automatically?</span>
|
||||
<small>If enabled, playlists will download without confirmation.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="setting-item">
|
||||
<input type="checkbox" data-key="notifySystem" id="notifySystem">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Notification at the end</span>
|
||||
<small>Notify when a download finishes.</small>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="customTopBar" id="customTopBar">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Custom Top bar</span>
|
||||
<small>Use this if the topbar is missing or you prefer a custom one.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="autoCheckInfo" id="autoCheckInfo">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Auto Fetch Info</span>
|
||||
<small>Automatically fetch information for new downloads.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Advanced Settings -->
|
||||
<details class="settings-section">
|
||||
<summary>Advanced Settings</summary>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="autoUpdate" id="autoUpdate">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Auto Update</span>
|
||||
<small>Enable automatic app updates.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="setting-item">
|
||||
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Output title (special chars)</span>
|
||||
<small>Check for unusual characters in output titles.</small>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="addMetadata" id="addMetadata">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Add Metadata</span>
|
||||
<small>Include metadata in downloaded files.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="addThumbnail" id="addThumbnail">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Add Thumbnail</span>
|
||||
<small>Include thumbnails in downloaded files.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item" style="margin-left: 32px; margin-top: 20px;">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Custom Codec</span>
|
||||
<select data-key="customCodec">
|
||||
<option value="h264">H264</option>
|
||||
<option value="av1">AV1</option>
|
||||
<option value="av01">AV01</option>
|
||||
<option value="vp9.2">VP9.2</option>
|
||||
<option value="vp9">VP9</option>
|
||||
<option value="h265">H265</option>
|
||||
<option value="theora">Theora</option>
|
||||
</select>
|
||||
<small>Choose your preferred video codec.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
<!-- Developer Settings -->
|
||||
<details class="settings-section">
|
||||
<summary>Developer Settings</summary>
|
||||
|
||||
<!-- <div class="setting-item">
|
||||
<input type="checkbox" data-key="downloadSystem" id="downloadSystem">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Download System</span>
|
||||
<small>Enable/disable the internal download system.</small>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- <div class="setting-item">
|
||||
<input type="checkbox" data-key="logSystem" id="logSystem">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Log System</span>
|
||||
<small>Enable logging system.</small>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="verboseLogs" id="verboseLogs">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Verbose Logs</span>
|
||||
<small>Show verbose logs for debugging purposes.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
<div class="settings-footer">
|
||||
<button id="open-json-btn">Open JSON Configuration</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container" id="container">
|
||||
|
||||
<header>
|
||||
<h1 id="title"> Freedom Loader </h1>
|
||||
<p id="subtitle"> Because why not? </p>
|
||||
</header>
|
||||
|
||||
<form action="/download" method="POST" id="downloadForm">
|
||||
<input name="url" placeholder="URL" id="UrlInput">
|
||||
|
||||
<label class="checkbox-inline">
|
||||
|
||||
<label class="checkbox-mp3">
|
||||
<input type="checkbox" name="audioOnly" value="1">
|
||||
<div class="checkmark"></div>
|
||||
</label>
|
||||
Audio Only (MP3)
|
||||
</label>
|
||||
<select name="quality">
|
||||
<option value="best">Best Quality</option>
|
||||
<option value="medium">Medium Quality</option>
|
||||
<option value="worst">Lowest Quality</option>
|
||||
<option value="1080">1080p</option>
|
||||
<option value="720">720p</option>
|
||||
<option value="480">480p</option>
|
||||
</select>
|
||||
|
||||
<button type="submit" onclick="startProgress()">Download</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="download-path">
|
||||
Will be save on <span id="savePath"></span>
|
||||
<button id="changePath">Edit</button>
|
||||
</div>
|
||||
|
||||
<div id="downloadProgressWrapper">
|
||||
<div id="downloadProgress"></div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px;">
|
||||
<div id="downloadProgressText"></div>
|
||||
<div id="downloadSpeedText"></div>
|
||||
</div>
|
||||
|
||||
<div id="downloadStatus"></div>
|
||||
|
||||
<div class="infos-container">
|
||||
<div id="videoInfo" style="margin-top: 20px;"></div>
|
||||
|
||||
<div id="loaderBox" class="loader-box" style="display:none;">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<span>Fetching info...</span>
|
||||
</div>
|
||||
|
||||
<div id="videoInfo" style="margin-top: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<span id="version-badge" class="version-badge"></span>
|
||||
|
||||
<h2 id="reference"> Because Why Not ? </h2>
|
||||
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="script/custompath.js"></script>
|
||||
<script src="script/settingsPanel.js"></script>
|
||||
<script src="script/appVersion.js"></script>
|
||||
<script src="script/downloadstatus.js"></script>
|
||||
<script src="script/fetchinfo.js"></script>
|
||||
|
||||
<script src="script/progressBar.js"></script>
|
||||
<script src="script/customthemes.js"></script>
|
||||
<script type="module" src="script/topbar.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
6
public/script/appVersion.js
Normal file
@@ -0,0 +1,6 @@
|
||||
async function versionLabel(){
|
||||
const appVersion = await window.electronAPI.getVersion();
|
||||
document.getElementById("version-badge").textContent = `v${appVersion}`;
|
||||
};
|
||||
|
||||
versionLabel();
|
||||
62
public/script/custompath.js
Normal file
@@ -0,0 +1,62 @@
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
const savePathElem = document.getElementById("savePath");
|
||||
const form = document.getElementById("downloadForm");
|
||||
|
||||
// input caché = DERNIER chemin validé par le back
|
||||
let hidden = document.getElementById("savePathInput");
|
||||
if (!hidden) {
|
||||
hidden = document.createElement("input");
|
||||
hidden.type = "hidden";
|
||||
hidden.name = "savePath";
|
||||
hidden.id = "savePathInput";
|
||||
form.appendChild(hidden);
|
||||
}
|
||||
|
||||
async function applyPathFromBack(path) {
|
||||
savePathElem.textContent = path;
|
||||
hidden.value = path;
|
||||
localStorage.setItem("customDownloadPath", path); // UX only
|
||||
}
|
||||
|
||||
async function loadInitialPath() {
|
||||
// On affiche ce que le user a vu la dernière fois (UX)
|
||||
const cached = localStorage.getItem("customDownloadPath");
|
||||
if (cached) {
|
||||
savePathElem.textContent = cached;
|
||||
}
|
||||
|
||||
// MAIS la source de vérité reste le back
|
||||
try {
|
||||
const validatedPath = await window.electronAPI.getValidatedDownloadPath(
|
||||
cached
|
||||
);
|
||||
await applyPathFromBack(validatedPath);
|
||||
} catch {
|
||||
// fallback sûr
|
||||
const defaultPath =
|
||||
await window.electronAPI.getDefaultDownloadPath();
|
||||
await applyPathFromBack(defaultPath);
|
||||
}
|
||||
}
|
||||
|
||||
await loadInitialPath();
|
||||
|
||||
document
|
||||
.getElementById("changePath")
|
||||
.addEventListener("click", async () => {
|
||||
try {
|
||||
const selectedPath =
|
||||
await window.electronAPI.selectDownloadFolder();
|
||||
|
||||
if (!selectedPath) return; // annulé
|
||||
|
||||
// Validation back obligatoire
|
||||
const validatedPath =
|
||||
await window.electronAPI.getValidatedDownloadPath(selectedPath);
|
||||
|
||||
await applyPathFromBack(validatedPath);
|
||||
} catch (err) {
|
||||
alert("Dossier non autorisé.");
|
||||
}
|
||||
});
|
||||
});
|
||||
62
public/script/customthemes.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const themes = {
|
||||
dark: { label: "Sombre", subtitle: "Darkness is my ally" },
|
||||
light: { label: "Clair", subtitle: "Qui aime ce thème ?" },
|
||||
neon: { label: "Purple", 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,73 +1,37 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
|
||||
Freedom Loader is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
|
||||
Freedom Loader is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Récupération du formulaire de téléchargement
|
||||
const form = document.getElementById("downloadForm");
|
||||
|
||||
// Div où on affiche l'état de progression (en cours, erreur, succès)
|
||||
const statusDiv = document.getElementById("downloadStatus");
|
||||
|
||||
// Récupération du bouton de validation pour pouvoir le désactiver pendant la requête
|
||||
const button = form.querySelector("button");
|
||||
|
||||
// On intercepte la soumission du formulaire
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault(); // empêche le rechargement de la page par le navigateur
|
||||
e.preventDefault();
|
||||
button.disabled = true; // Empêche les clics multiples
|
||||
statusDiv.textContent = "Download in progress...";
|
||||
|
||||
// On désactive le bouton pour éviter les clics multiples
|
||||
button.disabled = true;
|
||||
|
||||
// Affiche un message d'attente à l'utilisateur
|
||||
statusDiv.textContent = "Téléchargement en cours...";
|
||||
|
||||
// Prépare les données du formulaire sous forme URL-encoded
|
||||
const formData = new FormData(form);
|
||||
const params = new URLSearchParams(formData);
|
||||
|
||||
try {
|
||||
// Envoi de la requête POST vers /download avec les données du formulaire
|
||||
const res = await fetch("/download", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: params,
|
||||
});
|
||||
|
||||
// Si la réponse n'est pas un succès (code 4xx ou 5xx)
|
||||
if (!res.ok) {
|
||||
statusDiv.textContent = "❌ Erreur pendant le téléchargement.";
|
||||
statusDiv.textContent = res;
|
||||
return;
|
||||
}
|
||||
|
||||
// On récupère le texte de la réponse du serveur
|
||||
const text = await res.text();
|
||||
|
||||
// Affiche le message de fin de téléchargement (par exemple « Téléchargement terminé »)
|
||||
statusDiv.textContent = text;
|
||||
|
||||
} catch {
|
||||
// Gestion d'erreur en cas de problème réseau ou autre
|
||||
statusDiv.textContent = "❌ Une erreur s’est produite.";
|
||||
statusDiv.textContent = "❌ An Error has Occured.";
|
||||
} finally {
|
||||
// Réactive le bouton pour autoriser d'autres soumissions
|
||||
button.disabled = false;
|
||||
|
||||
// Efface le message de status après 5 secondes pour garder l'UI propre
|
||||
setTimeout(() => {
|
||||
statusDiv.textContent = "";
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,114 +1,191 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr || dateStr.length !== 8) return "Inconnue";
|
||||
return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`;
|
||||
}
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
function formatSize(bytes) {
|
||||
return bytes ? (bytes / (1024*1024)).toFixed(2) + " Mo" : "Inconnue";
|
||||
}
|
||||
|
||||
Freedom Loader is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
async function fetchVideoInfo(url) {
|
||||
try {
|
||||
const res = await fetch("/info", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ url }),
|
||||
});
|
||||
|
||||
Freedom Loader is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
if (!res.ok) return { error: `An Error occured when fetching info` };
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const data = await res.json();
|
||||
if (!data) return { error: "Data is Missing" };
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const urlInput = document.getElementById("UrlInput");
|
||||
const infoDiv = document.getElementById("videoInfo");
|
||||
return data;
|
||||
} catch (e) {
|
||||
return { error: "Network or JSON Issue" };
|
||||
}
|
||||
}
|
||||
|
||||
let lastFetchedUrl = "";
|
||||
const DEBUG = true;
|
||||
|
||||
urlInput.addEventListener("input", async () => {
|
||||
const url = urlInput.value.trim();
|
||||
if (DEBUG) console.log("[DEBUG] Input détecté :", url);
|
||||
async function init() {
|
||||
|
||||
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;
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const urlInput = document.getElementById("UrlInput");
|
||||
const infoDiv = document.getElementById("videoInfo");
|
||||
const loaderBox = document.getElementById("loaderBox");
|
||||
|
||||
if (url === lastFetchedUrl) {
|
||||
if (DEBUG) console.log("[DEBUG] Même URL que précédemment, pas de fetch.");
|
||||
return;
|
||||
}
|
||||
let lastFetchedUrl = "";
|
||||
|
||||
lastFetchedUrl = url;
|
||||
urlInput.addEventListener("input", async () => {
|
||||
|
||||
try {
|
||||
if (DEBUG) console.log("[DEBUG] Envoi requête POST vers /info avec URL:", url);
|
||||
const res = await fetch("/info", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ url }),
|
||||
});
|
||||
const configFeatures = await window.electronAPI.getFeatures();
|
||||
if (!configFeatures.autoCheckInfo) return;
|
||||
|
||||
if (DEBUG) console.log("[DEBUG] Réponse status:", res.status);
|
||||
const url = urlInput.value.trim();
|
||||
|
||||
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");
|
||||
// Si champ vide -> reset total
|
||||
if (!url || url.length < 2) {
|
||||
infoDiv.innerHTML = "";
|
||||
infoDiv.classList.remove("visible", "playlist-mode");
|
||||
lastFetchedUrl = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (DEBUG) console.log("[DEBUG] Données reçues:", data);
|
||||
if (url === lastFetchedUrl) return;
|
||||
lastFetchedUrl = url;
|
||||
|
||||
// Vérification minimaliste
|
||||
if (!data || !data.title || !data.thumbnail) {
|
||||
console.warn("[WARNING] Données incomplètes", data);
|
||||
infoDiv.innerHTML = "❌ Données incomplètes reçues.";
|
||||
infoDiv.classList.remove("visible");
|
||||
loaderBox.style.display = "flex";
|
||||
const data = await fetchVideoInfo(url);
|
||||
loaderBox.style.display = "none";
|
||||
|
||||
// Gestion des erreurs
|
||||
if (data.error) {
|
||||
infoDiv.innerHTML = `
|
||||
<div style="
|
||||
padding:7px;
|
||||
background:var(----infos-box-color);
|
||||
">
|
||||
<strong>${data.error}</strong>
|
||||
</div>
|
||||
`;
|
||||
infoDiv.classList.add("visible");
|
||||
infoDiv.classList.remove("playlist-mode");
|
||||
loaderBox.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(data.duration / 60);
|
||||
const seconds = data.duration % 60;
|
||||
const durationStr = `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
|
||||
// ---------- PLAYLIST ----------
|
||||
if (data.type === "playlist") {
|
||||
infoDiv.classList.add("playlist-mode");
|
||||
infoDiv.innerHTML = `
|
||||
<h3 style="color:var(--video-info-heading-color);"><strong>Playlist Detected: ${data.title}</strong></h3>
|
||||
<h3 style="color:var(--video-info-heading-color);"><strong>Video Count: ${data.count}</strong></h3>
|
||||
<p><strong>Channel :</strong> ${data.channel || "Unknown"}</p>
|
||||
<div id="playlistVideos"></div>
|
||||
`;
|
||||
|
||||
const date = data.upload_date || "";
|
||||
const readableDate = date
|
||||
? `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}`
|
||||
: "Inconnue";
|
||||
const listDiv = document.getElementById("playlistVideos");
|
||||
|
||||
const sizeMB = data.filesize_approx
|
||||
? (data.filesize_approx / (1024 * 1024)).toFixed(2) + " Mo"
|
||||
: "Inconnue";
|
||||
data.videos.forEach(v => {
|
||||
const durationStr = v.duration
|
||||
? `${Math.floor(v.duration / 60)}m ${(v.duration % 60).toString().padStart(2,"0")}s` : "Inconnue";
|
||||
|
||||
const categories = data.categories
|
||||
? data.categories.join(", ")
|
||||
: "Non spécifiées";
|
||||
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 = `
|
||||
<h3>${data.title}</h3>
|
||||
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
||||
<ul>
|
||||
<li><strong>Durée :</strong> ${durationStr}</li>
|
||||
<li><strong>Duration :</strong> ${durationStr}</li>
|
||||
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
|
||||
<li><strong>Date d’upload :</strong> ${readableDate}</li>
|
||||
<li><strong>Vues :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
|
||||
<li><strong>Upload Date :</strong> ${readableDate}</li>
|
||||
<li><strong>Views :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
|
||||
<li><strong>Likes :</strong> ${data.like_count?.toLocaleString() || "?"}</li>
|
||||
<li><strong>URL :</strong> <a href="${data.webpage_url}" target="_blank" rel="noopener noreferrer">${data.webpage_url}</a></li>
|
||||
<li><strong>Channel :</strong> <a href="${data.channel_url}" target="_blank" rel="noopener noreferrer">${data.channel_url}</a></li>
|
||||
<li><strong>Taille estimée :</strong> ${sizeMB}</li>
|
||||
<li><strong>Catégories :</strong> ${categories}</li>
|
||||
<li><strong>URL :</strong> <a href="${data.webpage_url}" target="_blank">${data.webpage_url}</a></li>
|
||||
<li><strong>Channel :</strong> <a href="${data.channel_url}" target="_blank">${data.channel_url}</a></li>
|
||||
<li><strong>Estimed Size :</strong> ${sizeStr}</li>
|
||||
<li><strong>Category :</strong> ${categories}</li>
|
||||
</ul>
|
||||
`;
|
||||
infoDiv.classList.add("visible");
|
||||
|
||||
if (DEBUG) console.log("[DEBUG] Info affichée avec succès.");
|
||||
} catch (e) {
|
||||
console.error("[CRITICAL] Erreur lors de la récupération:", e);
|
||||
infoDiv.innerHTML = "❌ Erreur réseau ou JSON.";
|
||||
infoDiv.classList.remove("visible");
|
||||
}
|
||||
});
|
||||
});
|
||||
infoDiv.classList.add("visible");
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
init();
|
||||
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"
|
||||
};
|
||||
50
public/script/settingsPanel.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const settingsPanel = document.getElementById("settings-panel");
|
||||
const settingsModal = document.querySelector(".settings-modal");
|
||||
|
||||
const settingsButton = document.getElementById("settings-button");
|
||||
|
||||
let isOpen = false;
|
||||
|
||||
settingsButton.addEventListener("click", () => {
|
||||
const isOpen = settingsPanel.style.display === "block";
|
||||
settingsPanel.style.display = isOpen ? "none" : "block";
|
||||
});
|
||||
|
||||
function closePanel() {
|
||||
isOpen = false;
|
||||
settingsPanel.style.display = "none";
|
||||
}
|
||||
|
||||
// charge les features
|
||||
async function loadSettings() {
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
|
||||
document.querySelectorAll(".settings-section input, .settings-section select").forEach(el => {
|
||||
const key = el.dataset.key;
|
||||
if (features[key] !== undefined) {
|
||||
if (el.type === "checkbox") el.checked = features[key];
|
||||
else if (el.tagName === "SELECT") el.value = features[key];
|
||||
}
|
||||
|
||||
el.addEventListener("change", () => {
|
||||
let value = el.type === "checkbox" ? el.checked : el.value;
|
||||
window.electronAPI.setFeature(key, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ouvrir le JSON
|
||||
document.getElementById("open-json-btn").addEventListener("click", () => {
|
||||
window.topbarAPI.openConfig(); // ton IPC existant
|
||||
});
|
||||
|
||||
|
||||
/* clic sur l'overlay = fermer */
|
||||
settingsPanel.addEventListener("click", closePanel);
|
||||
|
||||
/* clic dans la modal = stop propagation */
|
||||
settingsModal.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
loadSettings();
|
||||
34
public/script/topbar.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// Ajout des listeners pour la topbar
|
||||
function setupTopbarListeners() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const { topbarAPI } = window;
|
||||
if (!topbarAPI) return;
|
||||
const minBtn = document.getElementById('minimize-btn');
|
||||
const maxBtn = document.getElementById('maximize-btn');
|
||||
const closeBtn = document.getElementById('close-btn');
|
||||
const devtoolsBtn = document.getElementById('devtools-btn');
|
||||
const logsBtn = document.getElementById('logs-btn');
|
||||
const websiteBtn = document.getElementById('website-btn');
|
||||
const wikiBtn = document.getElementById('wiki-btn');
|
||||
const configBtn = document.getElementById('config-btn');
|
||||
|
||||
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
||||
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
|
||||
if (closeBtn) closeBtn.onclick = () => topbarAPI.close();
|
||||
if (devtoolsBtn) devtoolsBtn.onclick = () => topbarAPI.openDevTools();
|
||||
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
|
||||
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
|
||||
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
|
||||
if (configBtn) configBtn.onclick = () => topbarAPI.openConfig();
|
||||
});
|
||||
}
|
||||
|
||||
setupTopbarListeners(); // IF it put it the if check. It don't work. Why ?
|
||||
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
|
||||
if(!features.customTopBar){
|
||||
document.getElementById("topbar").style.display = "none";
|
||||
document.getElementById("container").style.marginTop = "0";
|
||||
document.getElementById("theme-switcher").style.top = "30px";
|
||||
}
|
||||
@@ -15,15 +15,15 @@
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
--clr: #3B82F6 ;
|
||||
--clr: var(--checkbox-bg-checked);
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 1.3em;
|
||||
width: 1.3em;
|
||||
background-color: #ccc;
|
||||
border-radius: 50%;
|
||||
transition: 300ms;
|
||||
background-color: var(--checkbox-bg-default);
|
||||
border-radius: .5rem;
|
||||
transition: background-color 300ms, border-radius 300ms;
|
||||
}
|
||||
|
||||
.checkbox-mp3 input:checked ~ .checkmark {
|
||||
@@ -47,7 +47,7 @@
|
||||
top: 0.25em;
|
||||
width: 0.25em;
|
||||
height: 0.5em;
|
||||
border: solid #E0E0E2;
|
||||
border: solid var(--checkbox-checkmark-border-color);
|
||||
border-width: 0 0.15em 0.15em 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
@@ -70,4 +70,4 @@
|
||||
box-shadow: 0 0 0 13px var(--checkbox-pulse-color);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,23 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* height: 10000px; */
|
||||
padding: 1em;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#downloadStatus {
|
||||
margin-top: 1em;
|
||||
min-height: 1.5em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: var(--download-status-color);
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#downloadStatus {
|
||||
|
||||
margin-top: 1em;
|
||||
min-height: 1.5em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
form {
|
||||
background-color: #fff;
|
||||
background-color: var(--form-bg-color);
|
||||
padding: 2em;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
@@ -13,8 +13,10 @@ form input[type="text"],
|
||||
form input[type="url"],
|
||||
form select {
|
||||
padding: 0.75em;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid var(--form-input-border-color);
|
||||
border-radius: 8px;
|
||||
background-color: var(--form-input-bg-color);
|
||||
color: var(--form-input-text-color);
|
||||
}
|
||||
|
||||
form input[type="checkbox"] {
|
||||
@@ -22,8 +24,8 @@ form input[type="checkbox"] {
|
||||
}
|
||||
|
||||
form button {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
background-color: var(--form-button-bg-color);
|
||||
color: var(--form-button-text-color);
|
||||
border: none;
|
||||
padding: 0.75em;
|
||||
border-radius: 8px;
|
||||
@@ -32,42 +34,43 @@ form button {
|
||||
}
|
||||
|
||||
form button:hover {
|
||||
background-color: #0056b3;
|
||||
background-color: var(--form-button-bg-hover-color);
|
||||
}
|
||||
|
||||
form#downloadForm {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkbox-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
gap: 0.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
gap: 0.5em;
|
||||
color: var(--form-input-text-color);
|
||||
}
|
||||
|
||||
#UrlInput {
|
||||
padding: 0.75em 1em;
|
||||
border-color: #ccc;
|
||||
box-shadow: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
padding: 0.75em 1em;
|
||||
border-color: var(--form-input-border-color);
|
||||
box-shadow: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
background-color: var(--form-input-bg-color);
|
||||
color: var(--form-input-text-color);
|
||||
}
|
||||
|
||||
#UrlInput:focus {
|
||||
border-color: #0056b3;
|
||||
box-shadow: 0 0 5px rgba(0,123,255,0.4);
|
||||
outline: none;
|
||||
background-color: #fff;
|
||||
border-color: var(--form-input-border-focus-color);
|
||||
box-shadow: 0 0 5px rgba(0,123,255,0.4);
|
||||
outline: none;
|
||||
background-color: var(--form-input-bg-color);
|
||||
}
|
||||
|
||||
#UrlInput::placeholder {
|
||||
color: #aaa;
|
||||
color: var(--form-input-placeholder-color);
|
||||
}
|
||||
|
||||
234
public/styles/layout/settingsPanel.css
Normal file
@@ -0,0 +1,234 @@
|
||||
/* SETTINGS BUTTON */
|
||||
.settings-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 20px;
|
||||
opacity: 0.8;
|
||||
background-color: var(--settings-button-bg-color);
|
||||
border-radius: 14px;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.25);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-button svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
stroke: var(--settings-button-text-color);
|
||||
}
|
||||
|
||||
.settings-button:hover {
|
||||
transform: scale(1.1);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.35);
|
||||
}
|
||||
|
||||
.settings-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
|
||||
/* SETTINGS PANEL */
|
||||
.settings-panel {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 40px;
|
||||
width: 40vw;
|
||||
max-width: 320px;
|
||||
max-height: 80vh;
|
||||
background-color: var(--settings-bg-primary-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
z-index: 9999;
|
||||
animation: slideIn 0.2s ease-out;
|
||||
color: var(--settings-text-color);
|
||||
}
|
||||
|
||||
.settings-panel::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.settings-modal {
|
||||
width: 100%;
|
||||
min-height: auto;
|
||||
max-height: none;
|
||||
padding: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: none;
|
||||
|
||||
}
|
||||
|
||||
/* TITLE */
|
||||
.settings-title {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.settings-title h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.settings-title p {
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* SECTION */
|
||||
.settings-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px 15px;
|
||||
background: var(--settings-bg-secondary-color);
|
||||
border-radius: 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* SETTING ITEM */
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.setting-item input[type="checkbox"] {
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.setting-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.setting-info small {
|
||||
font-size: 12px;
|
||||
color: var(--settings-subtitle-color);
|
||||
margin-top: 2px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#theme .settings-title{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.setting-item select {
|
||||
width: 70%;
|
||||
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;
|
||||
}
|
||||
|
||||
.setting-item select:hover,
|
||||
.setting-item select:focus {
|
||||
background: #474747;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* SELECT */
|
||||
.settings-section select {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* DETAILS / DROPDOWN */
|
||||
details {
|
||||
margin-bottom: 20px;
|
||||
padding: 10px 15px;
|
||||
background: #2a2a2a;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
details[open] {
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
details summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* FOOTER */
|
||||
.settings-footer {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#open-json-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
background-color: var(--form-button-bg-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#experimental-args {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
background-color: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
color: #fff;
|
||||
border: 1px solid #333;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* CLOSE BTN */
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ANIMATION */
|
||||
@keyframes popIn {
|
||||
from { transform: scale(0.95); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(20px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
116
public/styles/layout/topbar.css
Normal file
@@ -0,0 +1,116 @@
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 1100;
|
||||
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
|
||||
background: #1f1f1f98;
|
||||
color: #eaeaea;
|
||||
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
|
||||
}
|
||||
|
||||
/* LEFT - custom buttons */
|
||||
.custom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
/* RIGHT - window buttons */
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* GLOBAL BUTTON RESET */
|
||||
.topbar button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
transition: background 0.15s ease, transform 0.1s ease;
|
||||
|
||||
}
|
||||
|
||||
/* Custom buttons (Logs / Wiki / etc.) */
|
||||
.custom-controls button {
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
|
||||
font-size: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.custom-controls button:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
/* Window buttons (Windows style) */
|
||||
.window-controls button {
|
||||
width: 46px;
|
||||
height: 48px;
|
||||
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.window-controls button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.window-controls img {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
pointer-events: none;
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
|
||||
#close-btn:hover {
|
||||
background: #e81123;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Active effect */
|
||||
.topbar button:active {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
#close-btn:active {
|
||||
background: #c50f1f;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
position: fixed;
|
||||
right: 18px;
|
||||
bottom: 12px;
|
||||
z-index: 2000;
|
||||
font-size: 0.85em;
|
||||
color: #b0b0b0;
|
||||
background: none;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
padding: 2px 8px;
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
box-shadow: none;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
background-color: var(--infos-box-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||||
color: #222;
|
||||
color: var(--video-info-text-color);
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
opacity: 0;
|
||||
@@ -21,13 +21,12 @@
|
||||
#videoInfo.visible {
|
||||
opacity: 1;
|
||||
max-height: 1500px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#videoInfo h3 {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 0.6em;
|
||||
color: #007bff;
|
||||
color: var(--video-info-heading-color);
|
||||
font-weight: 700;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -51,15 +50,15 @@
|
||||
#videoInfo ul li {
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 500;
|
||||
color: #444;
|
||||
color: var(--video-info-list-color);
|
||||
}
|
||||
|
||||
#videoInfo ul li strong {
|
||||
color: #000;
|
||||
color: var(--video-info-list-strong-color);
|
||||
}
|
||||
|
||||
#videoInfo ul li a {
|
||||
color: #007bff;
|
||||
color: var(--video-info-link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -68,6 +67,81 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* --- Playlist Mode --- */
|
||||
#videoInfo.playlist-mode {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
/* Style du bloc "Playlist détectée" */
|
||||
#videoInfo.playlist-mode > p {
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Layout liste des vidéos */
|
||||
#playlistVideos {
|
||||
margin-top: 1em;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
/* Un item de playlist */
|
||||
#playlistVideos > div {
|
||||
background: var(--playlist-background-color);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
#playlistVideos > div p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#videoInfo.playlist-mode {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
#playlistVideos > div:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
#playlistVideos img {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
all: unset;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
transition: transform 0.1s ease, color 0.1s ease, opacity 0.15s ease;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
#playlistVideos > div p a{
|
||||
text-decoration: none;
|
||||
color: var(--video-info-link-color);
|
||||
}
|
||||
|
||||
#playlistVideos > div p a:hover{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 480px) {
|
||||
#videoInfo {
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
/* Layout */
|
||||
@import url("layout/form.css");
|
||||
@import url("layout/settingsPanel.css");
|
||||
@import url("layout/container.css");
|
||||
@import url("layout/ctr-infos.css");
|
||||
@import url("layout/header.css");
|
||||
@import url("layout/videoinfo.css");
|
||||
|
||||
/* Components */
|
||||
@import url("components/checkbox.css");
|
||||
@import url("variables.css");
|
||||
@import url("components/loader.css");
|
||||
|
||||
@import url("theme/themeimport.css");
|
||||
@import url("components/editpathbutton.css");
|
||||
@import url("components/progressBar.css");
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||
|
||||
#app {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s ease;
|
||||
}
|
||||
#app.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
html,body{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
scroll-behavior: smooth;
|
||||
/* outline: 2px solid red; */ /*<-- Utile pour le début ça */
|
||||
/*outline: 2px solid red; /*<-- Utile pour le debug ça */
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
@@ -21,10 +37,6 @@ html,body{
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: var(--background-color);
|
||||
/* background-image: linear-gradient(to left top, #838383, #a1a1a1, #bfbfbf, #dfdfdf, #ffffff);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed; */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--default-text-color);
|
||||
@@ -37,6 +49,57 @@ body {
|
||||
|
||||
}
|
||||
|
||||
/* Because why not */
|
||||
#reference{
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
bottom:0vh;
|
||||
left: 10px;
|
||||
font-size: 1em;
|
||||
color: #b0b0b02f;
|
||||
}
|
||||
|
||||
/* Scrollbar globale pour Chromium/Electron */
|
||||
body::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 5px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(255, 255, 255, 0.35);
|
||||
transform: scaleX(1.05);
|
||||
}
|
||||
|
||||
/* Thème clair */
|
||||
body.light::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
body.light::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
body.light::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.container{
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
header p{
|
||||
color: var(--subtitle-color);
|
||||
}
|
||||
@@ -45,22 +108,11 @@ header p{
|
||||
color: var(--mp3-text-color);
|
||||
}
|
||||
|
||||
/* #videoInfo {
|
||||
margin-top: 100px;
|
||||
padding: 1em;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-radius: 8px;
|
||||
color: rgb(0, 0, 0);
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
||||
transition: opacity 0.5s ease, max-height 0.5s ease;
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#videoInfo.visible {
|
||||
opacity: 1;
|
||||
max-height: 1000px;
|
||||
} */
|
||||
body,
|
||||
.container,
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
label {
|
||||
transition: background-color 0.2s ease, color 0.5s ease, border-color 0.2s ease;
|
||||
}
|
||||
103
public/styles/theme/chirac-theme.css
Normal file
@@ -0,0 +1,103 @@
|
||||
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;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #00196a;
|
||||
--settings-button-text-color: #dcdcdc;
|
||||
|
||||
--settings-bg-primary-color: #0b0528;
|
||||
--settings-bg-secondary-color: #1c1055;
|
||||
|
||||
--settings-text-color: #cecece;
|
||||
--settings-subtitle-color: #adadad;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
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
|
||||
|
||||
*/
|
||||
49
public/styles/theme/dark-theme.css
Normal file
@@ -0,0 +1,49 @@
|
||||
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;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #1e1e1e;
|
||||
--settings-button-text-color: var(--default-text-color);
|
||||
|
||||
--settings-bg-primary-color: #1e1e1e;
|
||||
--settings-bg-secondary-color: #2a2a2a;
|
||||
--settings-text-color: var(--default-text-color);
|
||||
--settings-subtitle-color: #aaa;
|
||||
|
||||
}
|
||||
61
public/styles/theme/drift-theme.css
Normal file
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #1e222ace;
|
||||
--settings-button-text-color: #b3b3b4;
|
||||
|
||||
--settings-bg-primary-color: #1e222a;
|
||||
--settings-bg-secondary-color: #272d38;
|
||||
|
||||
--settings-text-color: #bababa;
|
||||
--settings-subtitle-color: #cacaca;
|
||||
}
|
||||
|
||||
|
||||
|
||||
body.drift .download-path{
|
||||
color: #ededed;
|
||||
}
|
||||
49
public/styles/theme/fanatic-theme.css
Normal file
@@ -0,0 +1,49 @@
|
||||
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;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #1e1e1e;
|
||||
--settings-button-text-color: #dcdcdc;
|
||||
|
||||
--settings-bg-primary-color: #1e1e1e;
|
||||
--settings-bg-secondary-color: #2d2d2d;
|
||||
|
||||
--settings-text-color: #cecece;
|
||||
--settings-subtitle-color: #adadad;
|
||||
}
|
||||
48
public/styles/theme/light-theme.css
Normal file
@@ -0,0 +1,48 @@
|
||||
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;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #e4e4e4;
|
||||
--settings-button-text-color: var(--default-text-color);
|
||||
|
||||
--settings-bg-primary-color: #cbcbcb;
|
||||
--settings-bg-secondary-color: #cbcbcb;
|
||||
--settings-text-color: var(--default-text-color);
|
||||
--settings-subtitle-color: #656565;
|
||||
}
|
||||
59
public/styles/theme/neon-theme.css
Normal file
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #202038;
|
||||
--settings-button-text-color: var(--default-text-color);
|
||||
|
||||
--settings-bg-primary-color: #242454;
|
||||
--settings-bg-secondary-color: #2f2f6f;
|
||||
--settings-text-color: var(--default-text-color);
|
||||
--settings-subtitle-color: #d2d2d2;
|
||||
}
|
||||
|
||||
body.neon .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
58
public/styles/theme/nf-theme.css
Normal file
@@ -0,0 +1,58 @@
|
||||
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;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #2a2a2a;
|
||||
--settings-button-text-color: var(--default-text-color);
|
||||
|
||||
--settings-bg-primary-color: #2a2a2a;
|
||||
--settings-bg-secondary-color: #313131;
|
||||
--settings-text-color: var(--default-text-color);
|
||||
--settings-subtitle-color: #7a7979;
|
||||
}
|
||||
|
||||
body.nf .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
58
public/styles/theme/songbird-theme.css
Normal file
@@ -0,0 +1,58 @@
|
||||
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;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: rgba(29, 0, 0, 0.784);
|
||||
--settings-button-text-color: var(--default-text-color);
|
||||
|
||||
--settings-bg-primary-color: #121212;
|
||||
--settings-bg-secondary-color: #1a1a1a;
|
||||
--settings-text-color: var(--default-text-color);
|
||||
--settings-subtitle-color: #7a7979;
|
||||
}
|
||||
|
||||
body.cyberpunk .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
59
public/styles/theme/spicy-theme.css
Normal file
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #1a1a1a;
|
||||
--settings-button-text-color: #dcdcdc;
|
||||
|
||||
--settings-bg-primary-color: #1a1a1a;
|
||||
--settings-bg-secondary-color: #292929;
|
||||
|
||||
--settings-text-color: #cecece;
|
||||
--settings-subtitle-color: #adadad;
|
||||
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
9
public/styles/theme/themeimport.css
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
@import url("chirac-theme.css");
|
||||
@import url("dark-theme.css");
|
||||
@import url("fanatic-theme.css");
|
||||
@import url("light-theme.css");
|
||||
@import url("nf-theme.css");
|
||||
@import url("drift-theme.css");
|
||||
@import url("neon-theme.css");
|
||||
@import url("songbird-theme.css");
|
||||
@import url("spicy-theme.css")
|
||||
@@ -1,11 +1,46 @@
|
||||
:root{
|
||||
--background-color : #0F172A;
|
||||
--infos-box-color: #f1f1f1 ;
|
||||
--title-color: ;
|
||||
--subtitle-color : #a3aab4;
|
||||
--mp3-text-color: #0F172A;
|
||||
--default-text-color: #F8FAFC;
|
||||
:root {
|
||||
/* Couleurs générales */
|
||||
--background-color: #001224;
|
||||
--default-text-color: #eee;
|
||||
--subtitle-color: #007bff;
|
||||
--mp3-text-color: #000000;
|
||||
|
||||
--checkbox-pulse-color:#9bbcf1;
|
||||
/* Formulaire */
|
||||
--form-bg-color: #ffffff;
|
||||
--form-input-bg-color: #f9f9f9;
|
||||
--form-input-border-color: #cccccc;
|
||||
--form-input-border-focus-color: #0056b3;
|
||||
--form-input-text-color: #333333;
|
||||
--form-input-placeholder-color: #aaaaaa;
|
||||
--form-button-bg-color: #007bff;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #0056b3;
|
||||
|
||||
}
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
--checkbox-bg-checked: #3B82F6;
|
||||
--checkbox-checkmark-border-color: #E0E0E2;
|
||||
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #007bff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #f0f0f0;
|
||||
--video-info-text-color: #222222;
|
||||
--video-info-heading-color: #007bff;
|
||||
--video-info-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: var(--form-bg-color);
|
||||
--settings-button-text-color: var(--default-text-color);
|
||||
|
||||
--settings-bg-primary-color: var(--form-bg-color);
|
||||
--settings-bg-secondary-color: var(--form-bg-color);
|
||||
--settings-text-color: var(--default-text-color);
|
||||
--settings-subtitle-color: #aaa;
|
||||
|
||||
}
|
||||
|
||||
BIN
ressources/yt-dlp.exe
Normal file
64
server/controller/download.controller.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const { fetchDownload } = require("../services/download.services");
|
||||
const { logger } = require("../logger");
|
||||
const { isValidUrl, isSafePath } = require("../helpers/validation");
|
||||
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(`❌ Server Error`);
|
||||
}
|
||||
}
|
||||
|
||||
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};
|
||||
46
server/controller/info.controller.js
Normal file
@@ -0,0 +1,46 @@
|
||||
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 || typeof url !== "string" || !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) {
|
||||
logger.error(`Info controller error: ${err && err.message ? err.message : err}`);
|
||||
return res.status(500).send(`❌ Unable to fetch info`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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/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 };
|
||||
85
server/helpers/buildArgs.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const path = require("path");
|
||||
|
||||
const getUserBrowser = require("./getBrowser");
|
||||
const { ffmpegPath, denoPath} = require("./path");
|
||||
const { configFeatures } = require("../../config.js");
|
||||
const { logger } = require("../logger");
|
||||
|
||||
function validateCodec(codec){
|
||||
|
||||
const validCodec = [
|
||||
"av01","vp9.2", "vp9" , "h265" , "h264" , "vp8" , "h263", "theora"
|
||||
]
|
||||
|
||||
if(validCodec.includes(codec)){
|
||||
logger.info(`Codec Valid: ${codec}`)
|
||||
return codec
|
||||
} else{
|
||||
logger.error(`Codec not valid: ${codec}. Using default codec`)
|
||||
return "h264"
|
||||
}
|
||||
}
|
||||
|
||||
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||
|
||||
logger.info("--- CONFIGURATION ---");
|
||||
|
||||
logger.info(`CONFIG autoUpdate: ${configFeatures.autoUpdate}`);
|
||||
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
|
||||
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
|
||||
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
|
||||
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);
|
||||
logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`);
|
||||
logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`);
|
||||
logger.info(`CONFIG autoDownloadPlaylist: ${configFeatures.autoDownloadPlaylist}`);
|
||||
logger.info(`CONFIG customCodec: ${configFeatures.customCodec}`);
|
||||
|
||||
// ACTIVATE THEM WHEN THE FEATURE IS IMPLEMENTED
|
||||
// logger.info(`CONFIG logSystem: ${configFeatures.logSystem}`);
|
||||
// logger.info(`CONFIG outputTitleCheck: ${configFeatures.outputTitleCheck}`);
|
||||
// logger.info(`CONFIG downloadSystem: ${configFeatures.downloadSystem}`);
|
||||
// logger.info(`CONFIG notifySystem: ${configFeatures.notifySystem}`);
|
||||
|
||||
logger.info("---------------------");
|
||||
|
||||
const args = [
|
||||
configFeatures.verboseLogs ? "--verbose" : null, // Verbose Logs
|
||||
"--cookies-from-browser", `${getUserBrowser()}`,
|
||||
"--no-continue",
|
||||
"--no-overwrites",
|
||||
"--newline", // YT-DLP Logs Format
|
||||
configFeatures.addThumbnail ? "--embed-thumbnail" : null,
|
||||
configFeatures.addMetadata ? "--add-metadata" : null,
|
||||
"--concurrent-fragments", "8",
|
||||
"--retries", "10",
|
||||
"--fragment-retries", "10",
|
||||
"--ffmpeg-location", ffmpegPath,
|
||||
"--extractor-args","youtube:player_client=default",
|
||||
"--js-runtimes", `deno:${denoPath}`,
|
||||
"-S",
|
||||
`vcodec:${validateCodec(configFeatures.customCodec) || "h264"}`+
|
||||
`,acodec:aac`,
|
||||
configFeatures.autoDownloadPlaylist ? "--yes-playlist" : "--no-playlist"
|
||||
];
|
||||
|
||||
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.filter(Boolean);
|
||||
}
|
||||
|
||||
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 };
|
||||
33
server/helpers/path.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { app } = require("electron");
|
||||
const config = require("../../config");
|
||||
|
||||
const { logger } = require("../logger.js");
|
||||
|
||||
let userYtDlp;
|
||||
let ffmpegPath;
|
||||
let denoPath;
|
||||
|
||||
const sourceYtDlp = path.join(process.resourcesPath, "binaries","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, "binaries","ffmpeg.exe");
|
||||
denoPath = path.join(process.resourcesPath, "binaries","deno.exe");
|
||||
|
||||
if (!fs.existsSync(userYtDlp)) {
|
||||
fs.copyFileSync(sourceYtDlp, userYtDlp);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!userYtDlp){ logger.error("Missing YT-DLP")}
|
||||
if (!ffmpegPath){ logger.error("Missing FFMPEG")}
|
||||
if (!denoPath){ logger.error("Missing DENO")}
|
||||
|
||||
module.exports = { userYtDlp, ffmpegPath, denoPath };
|
||||
14
server/helpers/rateLimit.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const RateLimit = require("express-rate-limit");
|
||||
|
||||
|
||||
/* Rate Limite */
|
||||
const rateLimite = RateLimit({
|
||||
windowMs: 15 * 60, // 15 minutes
|
||||
max: 5, // limit each IP to 100 requests per windowMs on the root path
|
||||
message: "Too many requests, please try again later.",
|
||||
statusCode: 429, // HTTP status code for "Too Many Requests"
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
rateLimite
|
||||
}
|
||||
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 };
|
||||
108
server/logger.js
@@ -1,93 +1,61 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
const { createLogger, format, transports } = require("winston");
|
||||
const DailyRotateFile = require("winston-daily-rotate-file");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const config = require("../config")
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
// Dossier de logs Windows
|
||||
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
|
||||
|
||||
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.
|
||||
// Création du dossier "logs" si nécessaire
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
|
||||
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.
|
||||
const logFormat = format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
||||
);
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const { createLogger, format, transports } = require("winston");
|
||||
const DailyRotateFile = require("winston-daily-rotate-file");
|
||||
|
||||
const fs = require("fs"); // pour vérifier/créer le dossier de logs
|
||||
const path = require("path"); // pour gérer proprement les chemins
|
||||
|
||||
// on définit le dossier de logs
|
||||
const logDir = path.join(__dirname, "../logs");
|
||||
// on le crée si inexistant
|
||||
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir);
|
||||
|
||||
// fonction pour générer une ligne de début de session
|
||||
function getSessionStartLine() {
|
||||
const now = new Date().toISOString();
|
||||
return `--- Démarrage de la session : ${now} ---`;
|
||||
}
|
||||
|
||||
// fonction pour générer une ligne de fin de session
|
||||
function getSessionEndLine() {
|
||||
const now = new Date().toISOString();
|
||||
return `--- Fin de la session : ${now} ---`;
|
||||
}
|
||||
|
||||
// configuration principale de Winston
|
||||
// Configuration du logger
|
||||
const logger = createLogger({
|
||||
level: "info", // niveau de log minimum (info, warn, error, etc.)
|
||||
format: format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), // timestamp lisible
|
||||
format.printf(({ timestamp, level, message }) =>
|
||||
`${timestamp} | ${level.toUpperCase()} | ${message}` // format de ligne de log
|
||||
)
|
||||
),
|
||||
level: "info",
|
||||
format: logFormat,
|
||||
transports: [
|
||||
new DailyRotateFile({
|
||||
dirname: logDir, // dossier de logs
|
||||
filename: "LOGS-%DATE%.log", // nom de fichier journalier
|
||||
datePattern: "YYYY-MM-DD", // pattern de rotation
|
||||
zippedArchive: false, // true = compression des archives
|
||||
maxFiles: "14d", // garde 14 jours d’historique
|
||||
format: format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) =>
|
||||
`${timestamp} | ${level.toUpperCase()} | ${message}`
|
||||
)
|
||||
),
|
||||
options: { flags: "a" }, // mode append
|
||||
dirname: logDir,
|
||||
filename: "LOGS-%DATE%.log",
|
||||
datePattern: "YYYY-MM-DD",
|
||||
zippedArchive: false,
|
||||
maxFiles: "7d",
|
||||
format: logFormat,
|
||||
options: { flags: "a" },
|
||||
}),
|
||||
new transports.Console({
|
||||
format: format.combine(
|
||||
format.colorize(),
|
||||
format.timestamp({ format: "HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) =>
|
||||
`${timestamp} | ${level} | ${message}`
|
||||
)
|
||||
),
|
||||
format: logFormat,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// fonction d'utilitaire pour marquer le début de la session
|
||||
function logSessionStart() {
|
||||
logger.info(getSessionStartLine());
|
||||
function getSessionStartLine() {
|
||||
return `--- Starting session: ${new Date().toISOString()} ---`;
|
||||
}
|
||||
|
||||
function getSessionEndLine() {
|
||||
return `--- Ending session: ${new Date().toISOString()} ---`;
|
||||
}
|
||||
|
||||
function logSessionStart() {
|
||||
logger.info(getSessionStartLine());
|
||||
logger.info(`Application Version: ${config.version}`)
|
||||
}
|
||||
|
||||
// pour la fin de la session
|
||||
function logSessionEnd() {
|
||||
logger.info(getSessionEndLine());
|
||||
}
|
||||
|
||||
// exporte le logger et les helpers pour les autres modules
|
||||
module.exports = {
|
||||
logger,
|
||||
logSessionStart,
|
||||
logSessionEnd,
|
||||
};
|
||||
logDir,
|
||||
};
|
||||
@@ -1,165 +0,0 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
|
||||
Freedom Loader is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
|
||||
Freedom Loader is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const express = require("express"); // importe le framework Express
|
||||
const router = express.Router(); // crée un routeur Express pour modulariser les routes
|
||||
const { execFile } = require("child_process"); // permet d’exécuter un binaire externe sans passer par un shell
|
||||
const path = require("path"); // module Node.js pour gérer les chemins de fichiers
|
||||
const fs = require("fs"); // module Node.js pour manipuler le système de fichiers
|
||||
|
||||
const logger = require("../logger").logger; // récupère l'instance de logger Winston personnalisée
|
||||
|
||||
// chemin vers l’exécutable yt-dlp packagé avec l’appli
|
||||
const ytDlpPath = path.join(__dirname, '../../yt-dlp.exe');
|
||||
|
||||
// route POST sur la racine (ex: POST /download)
|
||||
router.post("/", (req, res) => {
|
||||
try {
|
||||
// récupère les options envoyées depuis le frontend (via formulaire ou requête AJAX)
|
||||
const options = {
|
||||
url: req.body.url, // URL à télécharger
|
||||
audioOnly: req.body.audioOnly === "1", // conversion audio uniquement (mp3)
|
||||
quality: req.body.quality || "best", // qualité vidéo à télécharger
|
||||
subtitles: req.body.subs === "1", // récupérer les sous-titres anglais
|
||||
};
|
||||
|
||||
if (!options.url) {
|
||||
// si l’URL est absente, on log et on renvoie 400
|
||||
logger.warn("Requête POST /download sans URL");
|
||||
return res.status(400).send("❌ URL manquante !");
|
||||
}
|
||||
|
||||
// on récupère le dossier de sortie depuis app.locals (défini ailleurs dans le serveur)
|
||||
const outputFolder = req.app.locals.outputFolder;
|
||||
|
||||
// s'il n'existe pas, on le crée automatiquement
|
||||
if (!fs.existsSync(outputFolder)) {
|
||||
fs.mkdirSync(outputFolder, { recursive: true });
|
||||
logger.info("Dossier Freedom Loader Output créé à la volée dans Téléchargements.");
|
||||
}
|
||||
|
||||
// template du nom de fichier de sortie (ex: MonTitre.mp4 ou MonTitre.mp3)
|
||||
const outputTemplate = path.join(outputFolder, "%(title)s.%(ext)s");
|
||||
|
||||
// tableau d'arguments pour yt-dlp
|
||||
const args = [];
|
||||
|
||||
if (options.audioOnly) {
|
||||
// si audioOnly, on force extraction audio en mp3
|
||||
args.push("--extract-audio", "--audio-format", "mp3");
|
||||
}
|
||||
|
||||
if (options.subtitles) {
|
||||
// si l’utilisateur veut des sous-titres
|
||||
args.push("--write-subs", "--sub-lang", "en");
|
||||
}
|
||||
|
||||
// // qualité souhaitée
|
||||
// args.push("-f", options.quality);
|
||||
|
||||
// let format;
|
||||
// if (options.quality === "best") {
|
||||
// format = "bestvideo+bestaudio/best";
|
||||
// } else if (options.quality === "worst") {
|
||||
// format = "worstvideo+worstaudio/worst";
|
||||
// } else {
|
||||
// format = "best"; // fallback
|
||||
// }
|
||||
// args.push("-f", format);
|
||||
|
||||
args.push("--no-continue");
|
||||
|
||||
let format;
|
||||
|
||||
switch (options.quality) {
|
||||
case "best":
|
||||
format = "bestvideo+bestaudio/best";
|
||||
break;
|
||||
case "medium":
|
||||
// qualité moyenne, ex: 720p vidéo + meilleur audio
|
||||
format = "bestvideo[height<=720]+bestaudio/best[height<=720]";
|
||||
break;
|
||||
case "worst":
|
||||
format = "worstvideo+worstaudio/worst";
|
||||
break;
|
||||
case "1080":
|
||||
format = "bestvideo[height<=1080]+bestaudio/best[height<=1080]";
|
||||
break;
|
||||
case "720":
|
||||
format = "bestvideo[height<=720]+bestaudio/best[height<=720]";
|
||||
break;
|
||||
case "480":
|
||||
format = "bestvideo[height<=480]+bestaudio/best[height<=480]";
|
||||
break;
|
||||
default:
|
||||
format = "best";
|
||||
}
|
||||
args.push("-f", format);
|
||||
|
||||
// modèle de sortie
|
||||
args.push("-o", outputTemplate);
|
||||
// enfin, l'URL à télécharger
|
||||
args.push(options.url);
|
||||
|
||||
// log de la commande complète pour debug
|
||||
logger.info(`Commande yt-dlp : ${ytDlpPath} ${args.join(" ")}`);
|
||||
|
||||
// exécution du binaire yt-dlp
|
||||
const child = execFile(ytDlpPath, args);
|
||||
|
||||
// gestion des logs stdout de yt-dlp
|
||||
child.stdout.on("data", (data) => {
|
||||
data.toString().split("\n").forEach(line => {
|
||||
if(line.trim()) logger.info(`[yt-dlp stdout] ${line.trim()}`);
|
||||
});
|
||||
});
|
||||
|
||||
// gestion des logs stderr de yt-dlp (les erreurs)
|
||||
child.stderr.on("data", (data) => {
|
||||
data.toString().split("\n").forEach(line => {
|
||||
if(line.trim()) logger.error(`[yt-dlp stderr] ${line.trim()}`);
|
||||
});
|
||||
});
|
||||
|
||||
// si une erreur système survient (par ex : yt-dlp.exe introuvable)
|
||||
child.on("error", (err) => {
|
||||
logger.error(`Erreur lancement yt-dlp : ${err.message}`);
|
||||
res.status(500).send(`❌ Erreur lors de l'exécution : ${err.message}`);
|
||||
});
|
||||
|
||||
// callback quand le processus se termine
|
||||
child.on("close", (code) => {
|
||||
logger.info(`yt-dlp terminé avec code de sortie : ${code}`);
|
||||
if(code === 0) {
|
||||
// succès
|
||||
res.send("✅ Téléchargement terminé !");
|
||||
} else {
|
||||
// yt-dlp a échoué
|
||||
res.status(500).send(`❌ yt-dlp a échoué avec le code : ${code}`);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
// en cas d'erreur JavaScript
|
||||
logger.error(`Erreur serveur dans /download : ${err.message}`);
|
||||
res.status(500).send(`Erreur serveur : ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// on exporte ce routeur pour qu'il soit utilisé par l'app principale
|
||||
module.exports = router;
|
||||
11
server/routes/download.route.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { downloadController, progressController, speedController } = require("../controller/download.controller");
|
||||
|
||||
router.post("/", downloadController);
|
||||
|
||||
// SSE pour la progression du téléchargement
|
||||
router.get("/progress", progressController);
|
||||
router.get("/speed", speedController);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
|
||||
Freedom Loader is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
|
||||
Freedom Loader is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const express = require("express"); // importe Express
|
||||
const router = express.Router(); // crée un routeur Express indépendant
|
||||
const { execFile } = require("child_process"); // permet d’exécuter un programme externe
|
||||
const path = require("path"); // module pour gérer les chemins de fichiers
|
||||
|
||||
// chemin vers le binaire yt-dlp embarqué
|
||||
const ytDlpPath = path.join(__dirname, "../../yt-dlp.exe");
|
||||
|
||||
// route POST sur la racine de ce routeur
|
||||
router.post("/", (req, res) => {
|
||||
const url = req.body.url; // récupère l'URL depuis la requête POST
|
||||
if (!url) return res.status(400).send("❌ URL manquante"); // validation simple
|
||||
|
||||
// exécute yt-dlp avec --dump-json pour récupérer les métadonnées de la vidéo
|
||||
execFile(ytDlpPath, ["--dump-json", url], (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
// gestion d’erreur : par exemple yt-dlp plante ou l’URL est invalide
|
||||
console.error("Erreur lors de l'exécution de yt-dlp :", error);
|
||||
console.error("stderr :", stderr);
|
||||
return res.status(500).send("❌ Impossible de récupérer les infos.");
|
||||
}
|
||||
|
||||
try {
|
||||
// on parse le JSON envoyé par yt-dlp
|
||||
const info = JSON.parse(stdout);
|
||||
// on renvoie l’objet JSON directement au frontend
|
||||
res.json(info);
|
||||
} catch (e) {
|
||||
// le JSON est mal formé
|
||||
console.error("Erreur lors du parsing JSON :", e);
|
||||
res.status(500).send("❌ JSON illisible.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// on exporte le routeur pour être utilisé dans app.js ou ailleurs
|
||||
module.exports = router;
|
||||
7
server/routes/info.route.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { infoController } = require("../controller/info.controller");
|
||||
|
||||
router.post("/", infoController);
|
||||
|
||||
module.exports = router;
|
||||
133
server/server.js
@@ -1,103 +1,70 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
const express = require("express");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { logger, logSessionEnd } = require("./logger");
|
||||
const config = require("../config");
|
||||
const { execFile } = require("child_process");
|
||||
const { userYtDlp } = require("./helpers/path");
|
||||
const { rateLimite } = require("./helpers/rateLimit")
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
const app = express();
|
||||
|
||||
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.
|
||||
app.use(express.json());
|
||||
|
||||
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.
|
||||
// Dossier de téléchargement
|
||||
const outputFolder = path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader");
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const express = require("express"); // framework web
|
||||
const { exec } = require("child_process"); // si besoin d’exécuter des commandes (pas utilisé ici directement)
|
||||
const fs = require("fs"); // accès au système de fichiers
|
||||
const path = require("path"); // gestion de chemins
|
||||
const debug = require("debug")("freedom-loader:server");
|
||||
// petit debugger coloré en console
|
||||
|
||||
const { logger, logSessionStart, logSessionEnd } = require("./logger");
|
||||
// récupère notre logger Winston + helpers de session
|
||||
|
||||
const app = express(); // instancie l'application Express
|
||||
|
||||
// logSessionStart(); // désactivé, mais pourrait logguer le début de session
|
||||
|
||||
// on définit le dossier de téléchargement dans le dossier Téléchargements de l’utilisateur
|
||||
const downloadsPath = path.join(process.env.USERPROFILE, "Downloads");
|
||||
const outputFolder = path.join(downloadsPath, "Freedom Loader Output");
|
||||
|
||||
// création du dossier s’il n’existe pas
|
||||
if (!fs.existsSync(outputFolder)) {
|
||||
try {
|
||||
fs.mkdirSync(outputFolder, { recursive: true });
|
||||
logger.info("Dossier Freedom Loader Output cree dans Telechargements.");
|
||||
} catch (err) {
|
||||
logger.error("Impossible de creer le dossier :", err);
|
||||
process.exit(1); // arrêt du programme en cas d’échec
|
||||
}
|
||||
} else {
|
||||
logger.info("Dossier Freedom Loader Output deja existant.");
|
||||
// Création du dossier si nécessaire
|
||||
try {
|
||||
fs.mkdirSync(outputFolder, { recursive: true });
|
||||
logger.info(`Freedom Loader folder ready in ${outputFolder}`);
|
||||
} catch (err) {
|
||||
logger.error("Unable to create folder:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// on rend ce dossier dispo via app.locals
|
||||
app.locals.outputFolder = outputFolder;
|
||||
// Mise à jour yt-dlp au
|
||||
execFile(userYtDlp, ["-U"], (err, stdout, stderr) => {
|
||||
if (err) logger.warn("yt-dlp update error:", err);
|
||||
else logger.info(`Update yt-dlp : ${stdout}`);
|
||||
});
|
||||
|
||||
// configuration pour parser les requêtes POST en urlencoded
|
||||
// Middlewares
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.static(path.join(__dirname, "../public")));
|
||||
|
||||
// sert les fichiers statiques (frontend)
|
||||
const staticPath = path.join(__dirname, "../public");
|
||||
debug("Serveur statique sur", staticPath);
|
||||
app.use(express.static(staticPath));
|
||||
// Routes
|
||||
app.use("/download", require("./routes/download.route"));
|
||||
app.use("/info", require("./routes/info.route"));
|
||||
|
||||
// enregistre les routes API
|
||||
const infoRoute = require("./routes/info");
|
||||
const downloadRoute = require("./routes/download");
|
||||
debug("Routes /download et /info installees");
|
||||
app.use("/download", downloadRoute);
|
||||
app.use("/info", infoRoute);
|
||||
|
||||
// route GET / de base
|
||||
app.get("/", (req, res) => {
|
||||
debug("Requete GET / servie");
|
||||
app.get("/", rateLimite ,(req, res) => {
|
||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||
});
|
||||
|
||||
// fonction pour démarrer le serveur (Promise pour pouvoir await ailleurs)
|
||||
function startServer() {
|
||||
// Fonction pour démarrer le serveur
|
||||
async function startServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.info("Demarrage du serveur Express...");
|
||||
const serverInstance = app.listen(8080, () => {
|
||||
logger.info("Serveur Express pret sur http://localhost:8080");
|
||||
resolve(serverInstance); // succès
|
||||
const server = app.listen(config.applicationPort, () => {
|
||||
logger.info(`Express server ready at http://localhost:${config.applicationPort}`);
|
||||
resolve(server);
|
||||
});
|
||||
serverInstance.on("error", (err) => {
|
||||
logger.error("Erreur serveur Express :", err);
|
||||
|
||||
server.on("error", (err) => {
|
||||
logger.error("Express Server Error :", err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
const gracefulExit = () => {
|
||||
logSessionEnd();
|
||||
server.close(() => {
|
||||
logger.info("Express Server close cleanly.");
|
||||
process.exit();
|
||||
});
|
||||
};
|
||||
|
||||
process.on("SIGINT", gracefulExit);
|
||||
process.on("SIGTERM", gracefulExit);
|
||||
});
|
||||
}
|
||||
|
||||
// gestion de la fermeture propre du process
|
||||
// process.on("exit", () => {
|
||||
// logSessionEnd();
|
||||
// });
|
||||
process.on("SIGINT", () => { // Ctrl+C
|
||||
logSessionEnd();
|
||||
process.exit();
|
||||
});
|
||||
process.on("SIGTERM", () => { // kill
|
||||
logSessionEnd();
|
||||
process.exit();
|
||||
});
|
||||
|
||||
// on exporte la fonction startServer pour que d’autres modules puissent la lancer
|
||||
module.exports = { startServer };
|
||||
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 };
|
||||
47
server/update.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
const { app, Notification, shell} = 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.`
|
||||
}).on("click", () =>
|
||||
shell.openExternal("https://github.com/MasterAcnolo/Freedom-Loader/releases/latest")
|
||||
).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 };
|
||||